Fix black chat pane and restore jumbo emoji rendering.
Column flex on keep-alive room hosts so the timeline fills the pane again, and route unicode jumbo through the plain emoji path with dedicated HTML parser options for custom emoticons.
This commit is contained in:
@@ -47,7 +47,16 @@ export function AnimatedOutlet() {
|
||||
pendingRoomId !== selectedRoomId;
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, minWidth: 0, minHeight: 0, position: 'relative', display: 'flex' }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{showOverlay && <PendingRoomShell roomId={pendingRoomId} />}
|
||||
{/*
|
||||
Do NOT key this by pathname — room switches share CachedRooms keep-alive.
|
||||
|
||||
@@ -46,6 +46,7 @@ type RenderMessageContentProps = {
|
||||
urlPreview?: boolean;
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
jumboHtmlReactParserOptions?: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
outlineAttachment?: boolean;
|
||||
disabledEmbedPatterns?: string[];
|
||||
@@ -60,6 +61,7 @@ export function RenderMessageContent({
|
||||
urlPreview,
|
||||
highlightRegex,
|
||||
htmlReactParserOptions,
|
||||
jumboHtmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
outlineAttachment,
|
||||
disabledEmbedPatterns,
|
||||
@@ -113,6 +115,7 @@ export function RenderMessageContent({
|
||||
{...props}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
@@ -187,6 +190,7 @@ export function RenderMessageContent({
|
||||
{...props}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
@@ -206,6 +210,7 @@ export function RenderMessageContent({
|
||||
{...props}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
@@ -224,6 +229,7 @@ export function RenderMessageContent({
|
||||
{...props}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { Opts } from 'linkifyjs';
|
||||
import { MessageEmptyContent } from './content';
|
||||
import { sanitizeCustomHtml } from '../../utils/sanitize';
|
||||
import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||
import { isMxEmoticonOnlyHtml } from '../../utils/regex';
|
||||
// Ensure jumbo emoji global styles are always in the bundle.
|
||||
import './layout/layout.css';
|
||||
|
||||
type RenderBodyProps = {
|
||||
body: string;
|
||||
@@ -13,6 +16,7 @@ type RenderBodyProps = {
|
||||
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
jumboHtmlReactParserOptions?: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
};
|
||||
export function RenderBody({
|
||||
@@ -21,13 +25,22 @@ export function RenderBody({
|
||||
jumbo = false,
|
||||
highlightRegex,
|
||||
htmlReactParserOptions,
|
||||
jumboHtmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
}: RenderBodyProps) {
|
||||
if (body === '') <MessageEmptyContent />;
|
||||
if (customBody) {
|
||||
if (customBody === '') <MessageEmptyContent />;
|
||||
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
|
||||
const hasCustomBody = typeof customBody === 'string' && customBody.length > 0;
|
||||
// Unicode jumbo should use the plain path so scaleSystemEmoji gets the jumbo flag.
|
||||
// HTML path is only needed for custom mx-emoticon images.
|
||||
const useCustomHtml = hasCustomBody && (!jumbo || isMxEmoticonOnlyHtml(customBody));
|
||||
|
||||
if (useCustomHtml) {
|
||||
const options =
|
||||
jumbo && jumboHtmlReactParserOptions ? jumboHtmlReactParserOptions : htmlReactParserOptions;
|
||||
return parse(sanitizeCustomHtml(customBody), options);
|
||||
}
|
||||
|
||||
if (body === '') return <MessageEmptyContent />;
|
||||
|
||||
return (
|
||||
<Linkify options={linkifyOpts}>
|
||||
{highlightRegex
|
||||
|
||||
@@ -20,7 +20,9 @@ const activeSlotStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
};
|
||||
@@ -29,11 +31,12 @@ const inactiveSlotStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
// Prefer opacity over visibility:hidden — the latter can reset overflow scrollTop.
|
||||
opacity: 0,
|
||||
flexDirection: 'column',
|
||||
// Scroll is saved/restored manually — hide without opacity so we don't stack
|
||||
// compositor layers over the active room (looked like an all-black chat pane).
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
// Keep in layout for scroll metrics; inert + aria-hidden handle a11y.
|
||||
zIndex: -1,
|
||||
};
|
||||
|
||||
type CachedRoomSlotProps = {
|
||||
@@ -175,8 +178,10 @@ export function CachedRooms() {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{ids.map((roomId) => {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const RoomFrame = memo(function RoomFrame({
|
||||
|
||||
return (
|
||||
<PowerLevelsContextProvider value={powerLevels}>
|
||||
<Box grow="Yes">
|
||||
<Box grow="Yes" direction="Column" style={{ minHeight: 0, minWidth: 0 }}>
|
||||
{forumRoom && !pending ? (
|
||||
<ForumRoomView room={room} eventId={eventId} />
|
||||
) : (
|
||||
|
||||
@@ -688,6 +688,17 @@ export const RoomTimeline = memo(function RoomTimeline({
|
||||
}),
|
||||
[mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler, useAuthentication]
|
||||
);
|
||||
const jumboHtmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(mx, room.roomId, {
|
||||
linkifyOpts,
|
||||
useAuthentication,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
jumbo: true,
|
||||
}),
|
||||
[mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler, useAuthentication]
|
||||
);
|
||||
const parseMemberEvent = useMemberEventParser();
|
||||
|
||||
const [timeline, setTimeline] = useState<Timeline>(() =>
|
||||
@@ -1410,6 +1421,7 @@ export const RoomTimeline = memo(function RoomTimeline({
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === MessageLayout.Bubble}
|
||||
disabledEmbedPatterns={combinedEmbedFilters}
|
||||
@@ -1518,6 +1530,7 @@ export const RoomTimeline = memo(function RoomTimeline({
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === MessageLayout.Bubble}
|
||||
disabledEmbedPatterns={combinedEmbedFilters}
|
||||
|
||||
@@ -212,6 +212,17 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) {
|
||||
}),
|
||||
[mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler, useAuthentication]
|
||||
);
|
||||
const jumboHtmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(mx, room.roomId, {
|
||||
linkifyOpts,
|
||||
useAuthentication,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
jumbo: true,
|
||||
}),
|
||||
[mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler, useAuthentication]
|
||||
);
|
||||
|
||||
const thread = room.getThread(threadRootId);
|
||||
const roomTimelineSet = room.getUnfilteredTimelineSet();
|
||||
@@ -634,6 +645,7 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) {
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === MessageLayout.Bubble}
|
||||
disabledEmbedPatterns={combinedEmbedFilters}
|
||||
@@ -650,6 +662,7 @@ export function ThreadView({ room, threadRootId }: ThreadViewProps) {
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
jumboHtmlReactParserOptions={jumboHtmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === MessageLayout.Bubble}
|
||||
disabledEmbedPatterns={combinedEmbedFilters}
|
||||
|
||||
@@ -37,6 +37,7 @@ export function DirectLayout() {
|
||||
minHeight: 0,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{keepHost && (
|
||||
@@ -46,6 +47,7 @@ export function DirectLayout() {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: showingRoom ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
aria-hidden={!showingRoom}
|
||||
@@ -59,6 +61,7 @@ export function DirectLayout() {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: showingRoom ? 'none' : 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -34,6 +34,7 @@ export function HomeLayout() {
|
||||
minHeight: 0,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{keepHost && (
|
||||
@@ -43,6 +44,7 @@ export function HomeLayout() {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: showingRoom ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
aria-hidden={!showingRoom}
|
||||
@@ -56,6 +58,7 @@ export function HomeLayout() {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: showingRoom ? 'none' : 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -46,6 +46,7 @@ export function SpaceRoomLayout() {
|
||||
minHeight: 0,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{keepHost && (
|
||||
@@ -55,6 +56,7 @@ export function SpaceRoomLayout() {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: showingRoom ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
aria-hidden={!showingRoom}
|
||||
@@ -68,6 +70,7 @@ export function SpaceRoomLayout() {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
display: showingRoom ? 'none' : 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -386,8 +386,10 @@ export const getReactCustomHtmlParser = (
|
||||
handleSpoilerClick?: ReactEventHandler<HTMLElement>;
|
||||
handleMentionClick?: ReactEventHandler<HTMLElement>;
|
||||
useAuthentication?: boolean;
|
||||
jumbo?: boolean;
|
||||
}
|
||||
): HTMLReactParserOptions => {
|
||||
const jumbo = params.jumbo ?? false;
|
||||
const opts: HTMLReactParserOptions = {
|
||||
replace: (domNode) => {
|
||||
if (domNode instanceof Element && 'name' in domNode) {
|
||||
@@ -443,6 +445,13 @@ export const getReactCustomHtmlParser = (
|
||||
}
|
||||
|
||||
if (name === 'p') {
|
||||
if (jumbo) {
|
||||
return (
|
||||
<p {...props} className={classNames(css.Paragraph, css.MarginSpaced)}>
|
||||
{domToReact(children, opts)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text {...props} className={classNames(css.Paragraph, css.MarginSpaced)} size="Inherit">
|
||||
{domToReact(children, opts)}
|
||||
@@ -561,9 +570,13 @@ export const getReactCustomHtmlParser = (
|
||||
// Drop presentational height/width so jumbo CSS can size freely
|
||||
const { height: _h, width: _w, ...imgProps } = props;
|
||||
return (
|
||||
<span className={css.EmoticonBase}>
|
||||
<span className={css.Emoticon()}>
|
||||
<AuthenticatedImg {...imgProps} className={css.EmoticonImg()} src={htmlSrc} />
|
||||
<span className={jumbo ? css.EmoticonBaseJumbo : css.EmoticonBase}>
|
||||
<span className={css.Emoticon({ jumbo })}>
|
||||
<AuthenticatedImg
|
||||
{...imgProps}
|
||||
className={css.EmoticonImg({ jumbo })}
|
||||
src={htmlSrc}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
@@ -577,7 +590,7 @@ export const getReactCustomHtmlParser = (
|
||||
!(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'code') &&
|
||||
!(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'a');
|
||||
|
||||
let jsx = scaleSystemEmoji(domNode.data);
|
||||
let jsx = scaleSystemEmoji(domNode.data, jumbo);
|
||||
|
||||
if (params.highlightRegex) {
|
||||
jsx = highlightText(params.highlightRegex, jsx);
|
||||
|
||||
@@ -31,6 +31,15 @@ const JUMBO_IGNORE_CHARS_REG = /[\u200B-\u200D\uFEFF\u00AD\u2060\u00A0]/g;
|
||||
const ONLY_MX_EMOTICON_HTML_REG =
|
||||
/^(?:\s|<br\s*\/?>)*((?:<img\b[^>]*\bdata-mx-emoticon\b[^>]*\/?>)(?:\s|<br\s*\/?>)*){1,10}$/i;
|
||||
|
||||
/** HTML that contains only custom Matrix emoticon images (no plain unicode body). */
|
||||
export const isMxEmoticonOnlyHtml = (html: string): boolean => {
|
||||
const normalized = html
|
||||
.replace(JUMBO_IGNORE_CHARS_REG, '')
|
||||
.trim()
|
||||
.replace(/(<br\s*\/?>\s*)+$/gi, '');
|
||||
return normalized.length > 0 && ONLY_MX_EMOTICON_HTML_REG.test(normalized);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -40,11 +49,7 @@ export const isJumboEmoji = (body: string, customBody?: string): boolean => {
|
||||
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;
|
||||
if (isMxEmoticonOnlyHtml(customBody)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user