From 0809c8d29a674d18198795dc16447c9c72934172 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Thu, 2 Apr 2026 02:32:35 -0400 Subject: [PATCH] fix(matrix): preserve legacy mention edits --- .../src/matrix/actions/messages.test.ts | 38 +++++++++++++++++ extensions/matrix/src/matrix/send.test.ts | 42 +++++++++++++++++++ extensions/matrix/src/matrix/send.ts | 27 +++++++++++- 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/extensions/matrix/src/matrix/actions/messages.test.ts b/extensions/matrix/src/matrix/actions/messages.test.ts index a173ba95aa8..bdf111c2f1e 100644 --- a/extensions/matrix/src/matrix/actions/messages.test.ts +++ b/extensions/matrix/src/matrix/actions/messages.test.ts @@ -148,6 +148,44 @@ describe("matrix message actions", () => { ); }); + it("does not re-notify legacy mentions when action edits target pre-m.mentions messages", async () => { + installMatrixActionTestRuntime(); + const sendMessage = vi.fn().mockResolvedValue("evt-edit"); + const client = { + getEvent: vi.fn().mockResolvedValue({ + content: { + body: "hello @alice:example.org", + }, + }), + getJoinedRoomMembers: vi.fn().mockResolvedValue([]), + getUserId: vi.fn().mockResolvedValue("@bot:example.org"), + sendMessage, + prepareForOneOff: vi.fn(async () => undefined), + start: vi.fn(async () => undefined), + stop: vi.fn(() => undefined), + stopAndPersist: vi.fn(async () => undefined), + } as unknown as MatrixClient; + + const result = await editMatrixMessage( + "!room:example.org", + "$original", + "hello again @alice:example.org", + { client }, + ); + + expect(result).toEqual({ eventId: "evt-edit" }); + expect(sendMessage).toHaveBeenCalledWith( + "!room:example.org", + expect.objectContaining({ + "m.mentions": {}, + "m.new_content": expect.objectContaining({ + body: "hello again @alice:example.org", + "m.mentions": { user_ids: ["@alice:example.org"] }, + }), + }), + ); + }); + it("includes poll snapshots when reading message history", async () => { const { client, doRequest, getEvent, getRelations } = createMessagesClient({ chunk: [ diff --git a/extensions/matrix/src/matrix/send.test.ts b/extensions/matrix/src/matrix/send.test.ts index 1c7397c2ebc..414d8c976a5 100644 --- a/extensions/matrix/src/matrix/send.test.ts +++ b/extensions/matrix/src/matrix/send.test.ts @@ -611,6 +611,48 @@ describe("editMessageMatrix mentions", () => { }, }); }); + + it("does not re-notify legacy mentions when the prior event body already mentioned the user", async () => { + const { client, sendMessage, getEvent } = makeClient(); + getEvent.mockResolvedValue({ + content: { + body: "hello @alice:example.org", + }, + }); + + await editMessageMatrix("room:!room:example", "$original", "hello again @alice:example.org", { + client, + }); + + expect(sendMessage.mock.calls[0]?.[1]).toMatchObject({ + "m.mentions": {}, + "m.new_content": { + body: "hello again @alice:example.org", + "m.mentions": { user_ids: ["@alice:example.org"] }, + }, + }); + }); + + it("keeps explicit empty prior m.mentions authoritative", async () => { + const { client, sendMessage, getEvent } = makeClient(); + getEvent.mockResolvedValue({ + content: { + body: "`@alice:example.org`", + "m.mentions": {}, + }, + }); + + await editMessageMatrix("room:!room:example", "$original", "@alice:example.org", { + client, + }); + + expect(sendMessage.mock.calls[0]?.[1]).toMatchObject({ + "m.mentions": { user_ids: ["@alice:example.org"] }, + "m.new_content": { + "m.mentions": { user_ids: ["@alice:example.org"] }, + }, + }); + }); }); describe("sendPollMatrix mentions", () => { diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 6fcc42cbea3..50c3b73a763 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -98,6 +98,27 @@ function resolvePreviousEditContent(previousEvent: unknown): Record | undefined): boolean { + return Boolean(content && Object.hasOwn(content, "m.mentions")); +} + +async function resolvePreviousEditMentions(params: { + client: MatrixClient; + content: Record | undefined; +}) { + if (hasMatrixMentionsMetadata(params.content)) { + return extractMatrixMentions(params.content); + } + const body = typeof params.content?.body === "string" ? params.content.body : ""; + if (!body) { + return {}; + } + return await resolveMatrixMentionsForBody({ + client: params.client, + body, + }); +} + export function prepareMatrixSingleText( text: string, opts: { @@ -459,9 +480,13 @@ export async function editMessageMatrix( }); const previousEvent = await getPreviousMatrixEvent(client, resolvedRoom, originalEventId); const previousContent = resolvePreviousEditContent(previousEvent); + const previousMentions = await resolvePreviousEditMentions({ + client, + content: previousContent, + }); const replaceMentions = diffMatrixMentions( extractMatrixMentions(newContent), - extractMatrixMentions(previousContent), + previousMentions, ); const replaceRelation: Record = {