From 8400f56004a1e64fbcc9d2e19ce23c74a72f06ae Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 13 May 2026 12:23:56 +0200 Subject: [PATCH] Fix edited reply quote collapse (#33487) * Fix edited reply quote collapse * Add regression test for edited reply quotes --- apps/web/res/css/views/rooms/_ReplyTile.pcss | 6 ++ .../views/elements/ReplyChain-test.tsx | 74 +++++++++++++++++++ .../body/TextualBodyView/TextualBodyView.tsx | 6 +- .../__snapshots__/TextualBody.test.tsx.snap | 1 + 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/apps/web/res/css/views/rooms/_ReplyTile.pcss b/apps/web/res/css/views/rooms/_ReplyTile.pcss index aea4bae626..d1c7e110f6 100644 --- a/apps/web/res/css/views/rooms/_ReplyTile.pcss +++ b/apps/web/res/css/views/rooms/_ReplyTile.pcss @@ -66,6 +66,12 @@ Please see LICENSE files in the repository root for full details. display: inline; } + // The annotation wrapper is flex in normal timeline messages, but that stops + // -webkit-line-clamp from trimming long edited quotes down to two lines. + [data-textual-body-annotation-wrapper] { + display: contents; + } + // Hide line numbers and edited indicator .mx_EventTile_lineNumbers, [data-textual-body-edited-marker] { diff --git a/apps/web/test/unit-tests/components/views/elements/ReplyChain-test.tsx b/apps/web/test/unit-tests/components/views/elements/ReplyChain-test.tsx index 4f750869c6..78b0afd9f9 100644 --- a/apps/web/test/unit-tests/components/views/elements/ReplyChain-test.tsx +++ b/apps/web/test/unit-tests/components/views/elements/ReplyChain-test.tsx @@ -59,4 +59,78 @@ describe("ReplyChain", () => { await waitFor(() => expect(setQuoteExpanded).toHaveBeenCalledWith(false)); expect(asFragment()).toMatchSnapshot(); }); + + it("keeps long edited reply quotes collapsible", async () => { + // Jest/JSDOM won't set clientHeight/scrollHeight for us so we have to synthesise it + jest.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(100); + jest.spyOn(Element.prototype, "scrollHeight", "get").mockReturnValue(150); + + const cli = stubClient(); + const { room_id: roomId } = await cli.createRoom({}); + const room = cli.getRoom(roomId)!; + const longBody = Array.from({ length: 80 }, (_, index) => `word${index}`).join(" "); + const editedLongBody = `${longBody} edited`; + + const targetEv = mkEvent({ + event: true, + type: "m.room.message", + user: cli.getUserId()!, + room: roomId, + id: "$event1", + content: { + body: longBody, + msgtype: "m.text", + }, + }); + const editEv = mkEvent({ + event: true, + type: "m.room.message", + user: cli.getUserId()!, + room: roomId, + id: "$event1-edit", + content: { + "body": `* ${editedLongBody}`, + "msgtype": "m.text", + "m.new_content": { + body: editedLongBody, + msgtype: "m.text", + }, + }, + }); + jest.spyOn(targetEv, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3)); + targetEv.makeReplaced(editEv); + jest.spyOn(room, "findEventById").mockReturnValue(targetEv); + + const parentEv = mkEvent({ + event: true, + type: "m.room.message", + user: cli.getUserId()!, + room: roomId, + id: "$event2", + content: { + "body": "Reply", + "msgtype": "m.text", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$event1", + }, + }, + }, + }); + const setQuoteExpanded = jest.fn(); + const { container } = render( + , + withClientContextRenderOptions(cli), + ); + + await waitFor(() => expect(setQuoteExpanded).toHaveBeenCalledWith(false)); + await waitFor(() => expect(container).toHaveTextContent(editedLongBody)); + + const replyTile = container.querySelector(".mx_ReplyTile"); + expect(replyTile).not.toBeNull(); + const annotationWrapper = replyTile!.querySelector("[data-textual-body-annotation-wrapper]"); + expect(annotationWrapper).not.toBeNull(); + expect(annotationWrapper).toContainElement(replyTile!.querySelector(".mx_EventTile_body")); + expect(annotationWrapper).toContainElement(replyTile!.querySelector("[data-textual-body-edited-marker]")); + }); }); diff --git a/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/TextualBodyView.tsx b/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/TextualBodyView.tsx index e4d0c022aa..e2f11b4aff 100644 --- a/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/TextualBodyView.tsx +++ b/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/TextualBodyView.tsx @@ -255,14 +255,16 @@ export function TextualBodyView({ [styles.annotatedInline]: kind === TextualBodyViewKind.EMOTE, }); + // Reply quotes need to tweak this wrapper so long edited messages still clamp nicely. + // Keep this hook stable so app CSS doesn't have to reach into CSS-module class names. renderedBody = kind === TextualBodyViewKind.EMOTE ? ( - + {renderedBody} {markers} ) : ( -
+
{renderedBody} {markers}
diff --git a/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/__snapshots__/TextualBody.test.tsx.snap b/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/__snapshots__/TextualBody.test.tsx.snap index e33064185f..fcbc847049 100644 --- a/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/__snapshots__/TextualBody.test.tsx.snap +++ b/packages/shared-components/src/room/timeline/event-tile/body/TextualBodyView/__snapshots__/TextualBody.test.tsx.snap @@ -33,6 +33,7 @@ exports[`TextualBodyView > renders emote messages with annotations 1`] = `