Fix msgtype changes from edits not rerendering timeline tiles (#33416)

* Fix msgtype changes from edits not rerendering timeline tiles

* Change approach to view model listeners instead

* Repaint the mounted tile on MatrixEventEvent.Replaced
This commit is contained in:
rbondesson
2026-05-08 09:33:23 +00:00
committed by GitHub
parent 1b9c9cb92f
commit a1c1d330e9
5 changed files with 126 additions and 4 deletions
@@ -620,6 +620,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
/** called when the event is edited after we show it. */
private readonly onReplaced = (): void => {
this.forceUpdate();
// re-verify the event if it is replaced (the edit may not be verified)
this.verifyEvent();
};
@@ -995,6 +996,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
public render(): ReactNode {
const msgtype = this.props.mxEvent.getContent().msgtype;
const eventType = this.props.mxEvent.getType();
const replacingEventId = this.props.mxEvent.replacingEventId();
const {
hasRenderer,
@@ -1335,6 +1337,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
// overrides
ref: this.tile,
replacingEventId,
isSeeingThroughMessageHiddenForModeration,
// appease TS
@@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type IContent, type MatrixClient, type MatrixEvent, MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
import {
type IContent,
type MatrixClient,
type MatrixEvent,
MatrixEventEvent,
MsgType,
PushRuleKind,
} from "matrix-js-sdk/src/matrix";
import parse from "html-react-parser";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import {
@@ -169,6 +176,8 @@ export class EventContentBodyViewModel
extends BaseViewModel<EventContentBodyViewSnapshot, EventContentBodyViewModelProps>
implements EventContentBodyViewModelInterface
{
private watchedEvent?: MatrixEvent;
private static readonly computeBodySnapshot = (
props: EventContentBodyViewModelProps,
): Pick<EventContentBodyViewSnapshot, "body" | "formattedBody" | "className"> => {
@@ -226,6 +235,8 @@ export class EventContentBodyViewModel
};
super(propsWithSettingDefaults, EventContentBodyViewModel.computeSnapshot(propsWithSettingDefaults));
this.watchEvent(propsWithSettingDefaults.mxEvent);
this.disposables.track(this.unwatchEvent);
const enableBigEmojiWatcherRef = SettingsStore.watchSetting(
"TextualBody.enableBigEmoji",
@@ -246,7 +257,26 @@ export class EventContentBodyViewModel
this.disposables.track(() => SettingsStore.unwatchSetting(shouldShowPillAvatarWatcherRef));
}
private readonly onEventContentChanged = (): void => {
const mxEvent = this.props.mxEvent;
if (!mxEvent) return;
this.setEventContent(mxEvent, mxEvent.getContent());
};
private watchEvent(mxEvent?: MatrixEvent): void {
if (this.watchedEvent === mxEvent) return;
this.unwatchEvent();
this.watchedEvent = mxEvent;
this.watchedEvent?.on(MatrixEventEvent.Replaced, this.onEventContentChanged);
}
private readonly unwatchEvent = (): void => {
this.watchedEvent?.off(MatrixEventEvent.Replaced, this.onEventContentChanged);
this.watchedEvent = undefined;
};
public setEventContent = (mxEvent: MatrixEvent | undefined, content: IContent): void => {
this.watchEvent(mxEvent);
this.props.mxEvent = mxEvent;
this.props.content = content;
const { body, formattedBody, className } = EventContentBodyViewModel.computeBodySnapshot(this.props);
@@ -6,7 +6,7 @@
*/
import React, { type MouseEvent } from "react";
import { MsgType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MatrixEventEvent, MsgType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import {
BaseViewModel,
LINKIFIED_DATA_ATTRIBUTE,
@@ -60,6 +60,8 @@ export class TextualBodyViewModel
extends BaseViewModel<TextualBodyViewSnapshot, TextualBodyViewModelProps>
implements TextualBodyViewModelInterface
{
private watchedEvent?: MatrixEvent;
private static readonly getKind = (mxEvent: MatrixEvent): TextualBodyViewKind => {
const msgtype = mxEvent.getContent().msgtype as MsgType | undefined;
@@ -120,7 +122,8 @@ export class TextualBodyViewModel
| "editedMarkerTooltip"
| "editedMarkerCaption"
> => {
if (!props.replacingEventId) {
const replacingEventId = props.replacingEventId ?? props.mxEvent.replacingEventId();
if (!replacingEventId) {
return {
showEditedMarker: false,
editedMarkerText: undefined,
@@ -197,8 +200,26 @@ export class TextualBodyViewModel
public constructor(props: TextualBodyViewModelProps) {
super(props, TextualBodyViewModel.computeSnapshot(props));
this.watchEvent(props.mxEvent);
this.disposables.track(this.unwatchEvent);
}
private readonly onEventContentChanged = (): void => {
this.snapshot.merge(TextualBodyViewModel.computeEventSnapshot(this.props));
};
private watchEvent(mxEvent: MatrixEvent): void {
if (this.watchedEvent === mxEvent) return;
this.unwatchEvent();
this.watchedEvent = mxEvent;
this.watchedEvent.on(MatrixEventEvent.Replaced, this.onEventContentChanged);
}
private readonly unwatchEvent = (): void => {
this.watchedEvent?.off(MatrixEventEvent.Replaced, this.onEventContentChanged);
this.watchedEvent = undefined;
};
public setId(id: string | undefined): void {
this.props = {
...this.props,
@@ -213,6 +234,7 @@ export class TextualBodyViewModel
...this.props,
mxEvent,
};
this.watchEvent(mxEvent);
this.snapshot.merge(TextualBodyViewModel.computeEventSnapshot(this.props));
}
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type ComponentProps } from "react";
import { type MatrixClient, type MatrixEvent, PushRuleKind, type Room } from "matrix-js-sdk/src/matrix";
import { mocked, type MockedObject } from "jest-mock";
import { render, waitFor } from "jest-matrix-react";
import { act, render, waitFor } from "jest-matrix-react";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import {
@@ -174,6 +174,44 @@ describe("<TextualBody />", () => {
expect(container).toHaveTextContent("* sender winks(edited)");
});
it("updates the body kind when an edit changes msgtype to m.emote", async () => {
DMRoomMap.makeShared(defaultMatrixClient);
const ev = mkEvent({
type: "m.room.message",
room: room1Id,
user: "sender",
content: {
body: "hello",
msgtype: "m.text",
},
event: true,
});
const { container } = getComponent({ mxEvent: ev });
expect(container).toHaveTextContent("hello");
const edit = mkEvent({
type: "m.room.message",
room: room1Id,
user: "sender",
content: {
"body": "* waves",
"msgtype": "m.emote",
"m.new_content": {
body: "waves",
msgtype: "m.emote",
},
},
event: true,
});
act(() => {
ev.makeReplaced(edit);
});
await waitFor(() => expect(container).toHaveTextContent("* sender waves(edited)"));
});
it("renders m.notice correctly", () => {
DMRoomMap.makeShared(defaultMatrixClient);
@@ -214,6 +214,35 @@ describe("EventTile", () => {
expect(screen.getByText("Can't load this message (m.room.message)")).toBeInTheDocument();
});
});
it("updates msgtype-derived tile classes when an edit changes msgtype to m.emote", async () => {
const { container } = getComponent();
expect(container.querySelector(".mx_EventTile_emote")).toBeNull();
const edit = new MatrixEvent({
type: EventType.RoomMessage,
room_id: ROOM_ID,
sender: "@alice:example.org",
content: {
"body": "* waves",
"msgtype": "m.emote",
"m.new_content": {
body: "waves",
msgtype: "m.emote",
},
"m.relates_to": {
rel_type: "m.replace",
event_id: mxEvent.getId(),
},
},
});
act(() => {
mxEvent.makeReplaced(edit);
});
await waitFor(() => expect(container.querySelector(".mx_EventTile_emote")).not.toBeNull());
});
});
describe("EventTile in the right panel", () => {