fix(matrix): preserve legacy mention edits

This commit is contained in:
Gustavo Madeira Santana 2026-04-02 02:32:35 -04:00
parent 3e52f5a021
commit 0809c8d29a
3 changed files with 106 additions and 1 deletions

View File

@ -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: [

View File

@ -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", () => {

View File

@ -98,6 +98,27 @@ function resolvePreviousEditContent(previousEvent: unknown): Record<string, unkn
: content;
}
function hasMatrixMentionsMetadata(content: Record<string, unknown> | undefined): boolean {
return Boolean(content && Object.hasOwn(content, "m.mentions"));
}
async function resolvePreviousEditMentions(params: {
client: MatrixClient;
content: Record<string, unknown> | 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<string, unknown> = {