Fix alignment of RTL messages (#12837)

* Fix alignment of RTL messages

Inspired by https://github.com/matrix-org/matrix-react-sdk/pull/5453 but
hopefully with the edited marker in the right place.

This is a PoC: types aren't correct and the style needs pulling
out to a class. Plus it would probably need more visual tests added.
If this looks acceptable, I can make these changes.

* Fix spacing between text and edited annotation

* Update snapshot

* Update more snapshots

* More snapshots

* More more snapshots

* Split out style

* Fix emotes

This will cause them always be right-justified if the display name
is rtl.

* Add playwright test for ltr/rtl message rendering

* Better snapshots

* Await on message sending

* Better waiting, hopefully

* Old snapshot files

* Really hopefully fixed screenshots this time

* Don't include the message action bar in the screenshots
This commit is contained in:
David Baker 2024-07-31 23:23:46 +01:00 committed by GitHub
parent f3ac6692da
commit a0c029c3c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 242 additions and 79 deletions

View file

@ -303,7 +303,6 @@ export interface EventRenderOpts {
disableBigEmoji?: boolean;
stripReplyFallback?: boolean;
forComposerQuote?: boolean;
ref?: React.Ref<HTMLSpanElement>;
}
function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): EventAnalysis {
@ -375,7 +374,61 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
}
}
export function bodyToNode(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): ReactNode {
export function bodyToDiv(
content: IContent,
highlights: Optional<string[]>,
opts: EventRenderOpts = {},
ref?: React.Ref<HTMLDivElement>,
): ReactNode {
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
return formattedBody ? (
<div
key="body"
ref={ref}
className={className}
dangerouslySetInnerHTML={{ __html: formattedBody }}
dir="auto"
/>
) : (
<div key="body" ref={ref} className={className} dir="auto">
{emojiBodyElements || strippedBody}
</div>
);
}
export function bodyToSpan(
content: IContent,
highlights: Optional<string[]>,
opts: EventRenderOpts = {},
ref?: React.Ref<HTMLSpanElement>,
includeDir = true,
): ReactNode {
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
return formattedBody ? (
<span
key="body"
ref={ref}
className={className}
dangerouslySetInnerHTML={{ __html: formattedBody }}
dir={includeDir ? "auto" : undefined}
/>
) : (
<span key="body" ref={ref} className={className} dir={includeDir ? "auto" : undefined}>
{emojiBodyElements || strippedBody}
</span>
);
}
interface BodyToNodeReturn {
strippedBody: string;
formattedBody?: string;
emojiBodyElements: JSX.Element[] | undefined;
className: string;
}
function bodyToNode(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): BodyToNodeReturn {
const eventInfo = analyseEvent(content, highlights, opts);
let emojiBody = false;
@ -419,19 +472,7 @@ export function bodyToNode(content: IContent, highlights: Optional<string[]>, op
emojiBodyElements = formatEmojis(eventInfo.strippedBody, false) as JSX.Element[];
}
return formattedBody ? (
<span
key="body"
ref={opts.ref}
className={className}
dangerouslySetInnerHTML={{ __html: formattedBody }}
dir="auto"
/>
) : (
<span key="body" ref={opts.ref} className={className} dir="auto">
{emojiBodyElements || eventInfo.strippedBody}
</span>
);
return { strippedBody: eventInfo.strippedBody, formattedBody, emojiBodyElements, className };
}
/**

View file

@ -172,7 +172,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
if (this.props.previousEdit) {
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
} else {
contentElements = HtmlUtils.bodyToNode(content, null, {
contentElements = HtmlUtils.bodyToSpan(content, null, {
stripReplyFallback: true,
});
}

View file

@ -59,7 +59,7 @@ interface IState {
}
export default class TextualBody extends React.Component<IBodyProps, IState> {
private readonly contentRef = createRef<HTMLSpanElement>();
private readonly contentRef = createRef<HTMLDivElement>();
private unmounted = false;
private pills: Element[] = [];
@ -566,34 +566,38 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
let isNotice = false;
let isEmote = false;
const isNotice = content.msgtype === MsgType.Notice;
const isEmote = content.msgtype === MsgType.Emote;
const willHaveWrapper =
this.props.replacingEventId || this.props.isSeeingThroughMessageHiddenForModeration || isEmote;
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);
isEmote = content.msgtype === MsgType.Emote;
isNotice = content.msgtype === MsgType.Notice;
let body = HtmlUtils.bodyToNode(content, this.props.highlights, {
const htmlOpts = {
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
// Part of Replies fallback support
stripReplyFallback: stripReply,
ref: this.contentRef,
});
};
let body = willHaveWrapper
? HtmlUtils.bodyToSpan(content, this.props.highlights, htmlOpts, this.contentRef, false)
: HtmlUtils.bodyToDiv(content, this.props.highlights, htmlOpts, this.contentRef);
if (this.props.replacingEventId) {
body = (
<>
<div dir="auto" className="mx_EventTile_annotated">
{body}
{this.renderEditedMarker()}
</>
</div>
);
}
if (this.props.isSeeingThroughMessageHiddenForModeration) {
body = (
<>
<div dir="auto" className="mx_EventTile_annotated">
{body}
{this.renderPendingModerationMarker()}
</>
</div>
);
}
@ -624,7 +628,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (isEmote) {
return (
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
*&nbsp;
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}