Default intentional mentions (#11602)

* Default intentional mentions

* wait for autocomplete to settle before submitting edit

* lint

* Update strings

---------

Co-authored-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Johannes Marbach 2023-09-14 13:36:15 +02:00 committed by GitHub
parent 237038aa56
commit 3608d52c4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 282 additions and 349 deletions

View file

@ -90,10 +90,9 @@ export function attachMentions(
replyToEvent: MatrixEvent | undefined, replyToEvent: MatrixEvent | undefined,
editedContent: IContent | null = null, editedContent: IContent | null = null,
): void { ): void {
// If this feature is disabled, do nothing. // We always attach the mentions even if the home server doesn't yet support
if (!SettingsStore.getValue("feature_intentional_mentions")) { // intentional mentions. This is safe because m.mentions is an additive change
return; // that should simply be ignored by incapable home servers.
}
// The mentions property *always* gets included to disable legacy push rules. // The mentions property *always* gets included to disable legacy push rules.
const mentions: IMentions = (content["m.mentions"] = {}); const mentions: IMentions = (content["m.mentions"] = {});

View file

@ -1151,7 +1151,6 @@
"voice_broadcast": "Voice broadcast", "voice_broadcast": "Voice broadcast",
"rust_crypto": "Rust cryptography implementation", "rust_crypto": "Rust cryptography implementation",
"hidebold": "Hide notification dot (only display counters badges)", "hidebold": "Hide notification dot (only display counters badges)",
"intentional_mentions": "Enable intentional mentions",
"ask_to_join": "Enable ask to join", "ask_to_join": "Enable ask to join",
"new_room_decoration_ui": "New room header & details interface", "new_room_decoration_ui": "New room header & details interface",
"beta_feature": "This is a beta feature", "beta_feature": "This is a beta feature",

View file

@ -531,20 +531,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
labsGroup: LabGroup.Rooms, labsGroup: LabGroup.Rooms,
default: false, default: false,
}, },
// MSC3952 intentional mentions support.
"feature_intentional_mentions": {
isFeature: true,
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td("labs|intentional_mentions"),
labsGroup: LabGroup.Rooms,
default: false,
controller: new ServerSupportUnstableFeatureController(
"feature_intentional_mentions",
defaultWatchManager,
[["org.matrix.msc3952_intentional_mentions"]],
"v1.7",
),
},
"feature_ask_to_join": { "feature_ask_to_join": {
default: false, default: false,
displayName: _td("labs|ask_to_join"), displayName: _td("labs|ask_to_join"),

View file

@ -23,7 +23,6 @@ import ContentMessages, { UploadCanceledError, uploadFile } from "../src/Content
import { doMaybeLocalRoomAction } from "../src/utils/local-room"; import { doMaybeLocalRoomAction } from "../src/utils/local-room";
import { createTestClient, mkEvent } from "./test-utils"; import { createTestClient, mkEvent } from "./test-utils";
import { BlurhashEncoder } from "../src/BlurhashEncoder"; import { BlurhashEncoder } from "../src/BlurhashEncoder";
import SettingsStore from "../src/settings/SettingsStore";
jest.mock("matrix-encrypt-attachment", () => ({ encryptAttachment: jest.fn().mockResolvedValue({}) })); jest.mock("matrix-encrypt-attachment", () => ({ encryptAttachment: jest.fn().mockResolvedValue({}) }));
@ -278,10 +277,6 @@ describe("ContentMessages", () => {
}); });
it("properly handles replies", async () => { it("properly handles replies", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_intentional_mentions",
);
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" }); mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const file = new File([], "fileName", { type: "image/jpeg" }); const file = new File([], "fileName", { type: "image/jpeg" });
const replyToEvent = mkEvent({ const replyToEvent = mkEvent({

View file

@ -112,6 +112,7 @@ describe("ThreadView", () => {
"rel_type": RelationType.Thread, "rel_type": RelationType.Thread,
}, },
"msgtype": MsgType.Text, "msgtype": MsgType.Text,
"m.mentions": {},
}; };
} }

View file

@ -137,13 +137,15 @@ describe("<EditMessageComposer/>", () => {
...editedEvent.getContent(), ...editedEvent.getContent(),
"body": " * original message + edit", "body": " * original message + edit",
"m.new_content": { "m.new_content": {
body: "original message + edit", "body": "original message + edit",
msgtype: "m.text", "msgtype": "m.text",
"m.mentions": {},
}, },
"m.relates_to": { "m.relates_to": {
event_id: editedEvent.getId(), event_id: editedEvent.getId(),
rel_type: "m.replace", rel_type: "m.replace",
}, },
"m.mentions": {},
}; };
expect(mockClient.sendMessage).toHaveBeenCalledWith(editedEvent.getRoomId()!, null, expectedBody); expect(mockClient.sendMessage).toHaveBeenCalledWith(editedEvent.getRoomId()!, null, expectedBody);
}); });
@ -168,13 +170,15 @@ describe("<EditMessageComposer/>", () => {
"body": " * hello world", "body": " * hello world",
"msgtype": "m.text", "msgtype": "m.text",
"m.new_content": { "m.new_content": {
body: "hello world", "body": "hello world",
msgtype: "m.text", "msgtype": "m.text",
"m.mentions": {},
}, },
"m.relates_to": { "m.relates_to": {
event_id: editedEvent.getId(), event_id: editedEvent.getId(),
rel_type: "m.replace", rel_type: "m.replace",
}, },
"m.mentions": {},
}); });
}); });
@ -191,15 +195,17 @@ describe("<EditMessageComposer/>", () => {
"format": "org.matrix.custom.html", "format": "org.matrix.custom.html",
"formatted_body": " * hello <em>world</em>", "formatted_body": " * hello <em>world</em>",
"m.new_content": { "m.new_content": {
body: "hello *world*", "body": "hello *world*",
msgtype: "m.text", "msgtype": "m.text",
format: "org.matrix.custom.html", "format": "org.matrix.custom.html",
formatted_body: "hello <em>world</em>", "formatted_body": "hello <em>world</em>",
"m.mentions": {},
}, },
"m.relates_to": { "m.relates_to": {
event_id: editedEvent.getId(), event_id: editedEvent.getId(),
rel_type: "m.replace", rel_type: "m.replace",
}, },
"m.mentions": {},
}); });
}); });
@ -216,15 +222,17 @@ describe("<EditMessageComposer/>", () => {
"format": "org.matrix.custom.html", "format": "org.matrix.custom.html",
"formatted_body": " * blinks <strong>quickly</strong>", "formatted_body": " * blinks <strong>quickly</strong>",
"m.new_content": { "m.new_content": {
body: "blinks __quickly__", "body": "blinks __quickly__",
msgtype: "m.emote", "msgtype": "m.emote",
format: "org.matrix.custom.html", "format": "org.matrix.custom.html",
formatted_body: "blinks <strong>quickly</strong>", "formatted_body": "blinks <strong>quickly</strong>",
"m.mentions": {},
}, },
"m.relates_to": { "m.relates_to": {
event_id: editedEvent.getId(), event_id: editedEvent.getId(),
rel_type: "m.replace", rel_type: "m.replace",
}, },
"m.mentions": {},
}); });
}); });
@ -240,13 +248,15 @@ describe("<EditMessageComposer/>", () => {
"body": " * ✨sparkles✨", "body": " * ✨sparkles✨",
"msgtype": "m.emote", "msgtype": "m.emote",
"m.new_content": { "m.new_content": {
body: "✨sparkles✨", "body": "✨sparkles✨",
msgtype: "m.emote", "msgtype": "m.emote",
"m.mentions": {},
}, },
"m.relates_to": { "m.relates_to": {
event_id: editedEvent.getId(), event_id: editedEvent.getId(),
rel_type: "m.replace", rel_type: "m.replace",
}, },
"m.mentions": {},
}); });
}); });
@ -264,166 +274,246 @@ describe("<EditMessageComposer/>", () => {
"body": " * //dev/null is my favourite place", "body": " * //dev/null is my favourite place",
"msgtype": "m.text", "msgtype": "m.text",
"m.new_content": { "m.new_content": {
body: "//dev/null is my favourite place", "body": "//dev/null is my favourite place",
msgtype: "m.text", "msgtype": "m.text",
"m.mentions": {},
}, },
"m.relates_to": { "m.relates_to": {
event_id: editedEvent.getId(), event_id: editedEvent.getId(),
rel_type: "m.replace", rel_type: "m.replace",
}, },
"m.mentions": {},
}); });
}); });
}); });
describe("with feature_intentional_mentions enabled", () => { describe("when message is not a reply", () => {
const mockSettings = (mockValues: Record<string, unknown> = {}) => { it("should attach an empty mentions object for a message with no mentions", async () => {
const defaultMockValues = { const editState = new EditorStateTransfer(editedEvent);
feature_intentional_mentions: true, getComponent(editState);
}; const editContent = " + edit";
jest.spyOn(SettingsStore, "getValue") await editText(editContent);
.mockClear()
.mockImplementation((settingName) => {
return { ...defaultMockValues, ...mockValues }[settingName];
});
};
beforeEach(() => { fireEvent.click(screen.getByText("Save"));
mockSettings();
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// both content.mentions and new_content.mentions are empty
expect(messageContent["m.mentions"]).toEqual({});
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({});
}); });
describe("when message is not a reply", () => { it("should retain mentions in the original message that are not removed by the edit", async () => {
it("should attach an empty mentions object for a message with no mentions", async () => { const editState = new EditorStateTransfer(eventWithMentions);
const editState = new EditorStateTransfer(editedEvent); getComponent(editState);
getComponent(editState); // Remove charlie from the message
const editContent = " + edit"; const editContent = "{backspace}{backspace}friends";
await editText(editContent); await editText(editContent);
fireEvent.click(screen.getByText("Save")); fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2]; const messageContent = mockClient.sendMessage.mock.calls[0][2];
// both content.mentions and new_content.mentions are empty // no new mentions were added, so nothing in top level mentions
expect(messageContent["m.mentions"]).toEqual({}); expect(messageContent["m.mentions"]).toEqual({});
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({}); // bob is still mentioned, charlie removed
}); expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: ["@bob:server.org"],
it("should retain mentions in the original message that are not removed by the edit", async () => {
const editState = new EditorStateTransfer(eventWithMentions);
getComponent(editState);
// Remove charlie from the message
const editContent = "{backspace}{backspace}friends";
await editText(editContent);
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// no new mentions were added, so nothing in top level mentions
expect(messageContent["m.mentions"]).toEqual({});
// bob is still mentioned, charlie removed
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: ["@bob:server.org"],
});
});
it("should remove mentions that are removed by the edit", async () => {
const editState = new EditorStateTransfer(eventWithMentions);
getComponent(editState);
const editContent = "new message!";
// clear the original message
await editText(editContent, true);
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// no new mentions were added, so nothing in top level mentions
expect(messageContent["m.mentions"]).toEqual({});
// bob is not longer mentioned in the edited message, so empty mentions in new_content
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({});
});
it("should add mentions that were added in the edit", async () => {
const editState = new EditorStateTransfer(editedEvent);
getComponent(editState);
const editContent = " and @d";
await editText(editContent);
// submit autocomplete for mention
await editText("{enter}");
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// new mention in the edit
expect(messageContent["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
});
it("should add and remove mentions from the edit", async () => {
const editState = new EditorStateTransfer(eventWithMentions);
getComponent(editState);
// Remove charlie from the message
await editText("{backspace}{backspace}");
// and replace with @room
await editText("@d");
// submit autocomplete for @dan mention
await editText("{enter}");
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// new mention in the edit
expect(messageContent["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
// all mentions in the edited version of the event
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: ["@bob:server.org", "@dan:server.org"],
});
}); });
}); });
describe("when message is replying", () => { it("should remove mentions that are removed by the edit", async () => {
const originalEvent = mkEvent({ const editState = new EditorStateTransfer(eventWithMentions);
type: "m.room.message", getComponent(editState);
user: "@ernie:test", const editContent = "new message!";
room: roomId, // clear the original message
content: { body: "original message", msgtype: "m.text" }, await editText(editContent, true);
event: true,
});
const replyEvent = mkEvent({ fireEvent.click(screen.getByText("Save"));
type: "m.room.message",
user: "@bert:test", const messageContent = mockClient.sendMessage.mock.calls[0][2];
room: roomId,
content: { // no new mentions were added, so nothing in top level mentions
"body": "reply with plain message", expect(messageContent["m.mentions"]).toEqual({});
"msgtype": "m.text", // bob is not longer mentioned in the edited message, so empty mentions in new_content
"m.relates_to": { expect(messageContent["m.new_content"]["m.mentions"]).toEqual({});
"m.in_reply_to": { });
event_id: originalEvent.getId(),
}, it("should add mentions that were added in the edit", async () => {
}, const editState = new EditorStateTransfer(editedEvent);
"m.mentions": { getComponent(editState);
user_ids: [originalEvent.getSender()!], const editContent = " and @d";
await editText(editContent);
// wait for autocompletion to render
await screen.findByText("Dan");
// submit autocomplete for mention
await editText("{enter}");
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// new mention in the edit
expect(messageContent["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
});
it("should add and remove mentions from the edit", async () => {
const editState = new EditorStateTransfer(eventWithMentions);
getComponent(editState);
// Remove charlie from the message
await editText("{backspace}{backspace}");
// and replace with @room
await editText("@d");
// wait for autocompletion to render
await screen.findByText("Dan");
// submit autocomplete for @dan mention
await editText("{enter}");
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// new mention in the edit
expect(messageContent["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
// all mentions in the edited version of the event
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: ["@bob:server.org", "@dan:server.org"],
});
});
});
describe("when message is replying", () => {
const originalEvent = mkEvent({
type: "m.room.message",
user: "@ernie:test",
room: roomId,
content: { body: "original message", msgtype: "m.text" },
event: true,
});
const replyEvent = mkEvent({
type: "m.room.message",
user: "@bert:test",
room: roomId,
content: {
"body": "reply with plain message",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
event_id: originalEvent.getId(),
}, },
}, },
event: true, "m.mentions": {
}); user_ids: [originalEvent.getSender()!],
},
},
event: true,
});
const replyWithMentions = mkEvent({ const replyWithMentions = mkEvent({
type: "m.room.message",
user: "@bert:test",
room: roomId,
content: {
"body": 'reply that mentions <a href="https://matrix.to/#/@bob:server.org">Bob</a>',
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
event_id: originalEvent.getId(),
},
},
"m.mentions": {
user_ids: [
// sender of event we replied to
originalEvent.getSender()!,
// mentions from this event
"@bob:server.org",
],
},
},
event: true,
});
beforeEach(() => {
setupRoomWithEventsTimeline(room, [originalEvent, replyEvent]);
});
it("should retain parent event sender in mentions when editing with plain text", async () => {
const editState = new EditorStateTransfer(replyEvent);
getComponent(editState);
const editContent = " + edit";
await editText(editContent);
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// no new mentions from edit
expect(messageContent["m.mentions"]).toEqual({});
// edited reply still mentions the parent event sender
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: [originalEvent.getSender()],
});
});
it("should retain parent event sender in mentions when adding a mention", async () => {
const editState = new EditorStateTransfer(replyEvent);
getComponent(editState);
await editText(" and @d");
// wait for autocompletion to render
await screen.findByText("Dan");
// submit autocomplete for @dan mention
await editText("{enter}");
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// new mention in edit
expect(messageContent["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
// edited reply still mentions the parent event sender
// plus new mention @dan
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: [originalEvent.getSender(), "@dan:server.org"],
});
});
it("should retain parent event sender in mentions when removing all mentions from content", async () => {
const editState = new EditorStateTransfer(replyWithMentions);
getComponent(editState);
// replace text to remove all mentions
await editText("no mentions here", true);
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// no mentions in edit
expect(messageContent["m.mentions"]).toEqual({});
// edited reply still mentions the parent event sender
// existing @bob mention removed
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: [originalEvent.getSender()],
});
});
it("should retain parent event sender in mentions when removing mention of said user", async () => {
const replyThatMentionsParentEventSender = mkEvent({
type: "m.room.message", type: "m.room.message",
user: "@bert:test", user: "@bert:test",
room: roomId, room: roomId,
content: { content: {
"body": 'reply that mentions <a href="https://matrix.to/#/@bob:server.org">Bob</a>', "body": `reply that mentions the sender of the message we replied to <a href="https://matrix.to/#/${originalEvent.getSender()!}">Ernie</a>`,
"msgtype": "m.text", "msgtype": "m.text",
"m.relates_to": { "m.relates_to": {
"m.in_reply_to": { "m.in_reply_to": {
@ -434,114 +524,25 @@ describe("<EditMessageComposer/>", () => {
user_ids: [ user_ids: [
// sender of event we replied to // sender of event we replied to
originalEvent.getSender()!, originalEvent.getSender()!,
// mentions from this event
"@bob:server.org",
], ],
}, },
}, },
event: true, event: true,
}); });
const editState = new EditorStateTransfer(replyThatMentionsParentEventSender);
getComponent(editState);
// replace text to remove all mentions
await editText("no mentions here", true);
beforeEach(() => { fireEvent.click(screen.getByText("Save"));
setupRoomWithEventsTimeline(room, [originalEvent, replyEvent]);
});
it("should retain parent event sender in mentions when editing with plain text", async () => { const messageContent = mockClient.sendMessage.mock.calls[0][2];
const editState = new EditorStateTransfer(replyEvent);
getComponent(editState);
const editContent = " + edit";
await editText(editContent);
fireEvent.click(screen.getByText("Save")); // no mentions in edit
expect(messageContent["m.mentions"]).toEqual({});
const messageContent = mockClient.sendMessage.mock.calls[0][2]; // edited reply still mentions the parent event sender
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
// no new mentions from edit user_ids: [originalEvent.getSender()],
expect(messageContent["m.mentions"]).toEqual({});
// edited reply still mentions the parent event sender
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: [originalEvent.getSender()],
});
});
it("should retain parent event sender in mentions when adding a mention", async () => {
const editState = new EditorStateTransfer(replyEvent);
getComponent(editState);
await editText(" and @d");
// submit autocomplete for @dan mention
await editText("{enter}");
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// new mention in edit
expect(messageContent["m.mentions"]).toEqual({
user_ids: ["@dan:server.org"],
});
// edited reply still mentions the parent event sender
// plus new mention @dan
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: [originalEvent.getSender(), "@dan:server.org"],
});
});
it("should retain parent event sender in mentions when removing all mentions from content", async () => {
const editState = new EditorStateTransfer(replyWithMentions);
getComponent(editState);
// replace text to remove all mentions
await editText("no mentions here", true);
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// no mentions in edit
expect(messageContent["m.mentions"]).toEqual({});
// edited reply still mentions the parent event sender
// existing @bob mention removed
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: [originalEvent.getSender()],
});
});
it("should retain parent event sender in mentions when removing mention of said user", async () => {
const replyThatMentionsParentEventSender = mkEvent({
type: "m.room.message",
user: "@bert:test",
room: roomId,
content: {
"body": `reply that mentions the sender of the message we replied to <a href="https://matrix.to/#/${originalEvent.getSender()!}">Ernie</a>`,
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
event_id: originalEvent.getId(),
},
},
"m.mentions": {
user_ids: [
// sender of event we replied to
originalEvent.getSender()!,
],
},
},
event: true,
});
const editState = new EditorStateTransfer(replyThatMentionsParentEventSender);
getComponent(editState);
// replace text to remove all mentions
await editText("no mentions here", true);
fireEvent.click(screen.getByText("Save"));
const messageContent = mockClient.sendMessage.mock.calls[0][2];
// no mentions in edit
expect(messageContent["m.mentions"]).toEqual({});
// edited reply still mentions the parent event sender
expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
user_ids: [originalEvent.getSender()],
});
}); });
}); });
}); });

View file

@ -39,7 +39,6 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink
import { mockPlatformPeg } from "../../../test-utils/platform"; import { mockPlatformPeg } from "../../../test-utils/platform";
import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room"; import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room";
import { addTextToComposer } from "../../../test-utils/composer"; import { addTextToComposer } from "../../../test-utils/composer";
import SettingsStore from "../../../../src/settings/SettingsStore";
jest.mock("../../../../src/utils/local-room", () => ({ jest.mock("../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(), doMaybeLocalRoomAction: jest.fn(),
@ -97,8 +96,9 @@ describe("<SendMessageComposer/>", () => {
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator); const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "hello world", "body": "hello world",
msgtype: "m.text", "msgtype": "m.text",
"m.mentions": {},
}); });
}); });
@ -110,10 +110,11 @@ describe("<SendMessageComposer/>", () => {
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator); const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "hello *world*", "body": "hello *world*",
msgtype: "m.text", "msgtype": "m.text",
format: "org.matrix.custom.html", "format": "org.matrix.custom.html",
formatted_body: "hello <em>world</em>", "formatted_body": "hello <em>world</em>",
"m.mentions": {},
}); });
}); });
@ -125,10 +126,11 @@ describe("<SendMessageComposer/>", () => {
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator); const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "blinks __quickly__", "body": "blinks __quickly__",
msgtype: "m.emote", "msgtype": "m.emote",
format: "org.matrix.custom.html", "format": "org.matrix.custom.html",
formatted_body: "blinks <strong>quickly</strong>", "formatted_body": "blinks <strong>quickly</strong>",
"m.mentions": {},
}); });
}); });
@ -141,8 +143,9 @@ describe("<SendMessageComposer/>", () => {
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator); const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "✨sparkles✨", "body": "✨sparkles✨",
msgtype: "m.emote", "msgtype": "m.emote",
"m.mentions": {},
}); });
}); });
@ -155,23 +158,14 @@ describe("<SendMessageComposer/>", () => {
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator); const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
expect(content).toEqual({ expect(content).toEqual({
body: "/dev/null is my favourite place", "body": "/dev/null is my favourite place",
msgtype: "m.text", "msgtype": "m.text",
"m.mentions": {},
}); });
}); });
}); });
describe("attachMentions", () => { describe("attachMentions", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_intentional_mentions",
);
});
afterEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReset();
});
const partsCreator = createPartCreator(); const partsCreator = createPartCreator();
it("no mentions", () => { it("no mentions", () => {
@ -488,8 +482,9 @@ describe("<SendMessageComposer/>", () => {
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, { expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, {
body: "test message", "body": "test message",
msgtype: MsgType.Text, "msgtype": MsgType.Text,
"m.mentions": {},
}); });
}); });
@ -507,8 +502,9 @@ describe("<SendMessageComposer/>", () => {
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, { expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, {
body: "test message", "body": "test message",
msgtype: MsgType.Text, "msgtype": MsgType.Text,
"m.mentions": {},
}); });
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: `effects.confetti` }); expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: `effects.confetti` });
@ -534,8 +530,9 @@ describe("<SendMessageComposer/>", () => {
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, { expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, {
body: "test message", "body": "test message",
msgtype: MsgType.Text, "msgtype": MsgType.Text,
"m.mentions": {},
}); });
expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: `effects.confetti` }); expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: `effects.confetti` });

View file

@ -27,7 +27,6 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink
import { VoiceRecordingStore } from "../../../../src/stores/VoiceRecordingStore"; import { VoiceRecordingStore } from "../../../../src/stores/VoiceRecordingStore";
import { PlaybackClock } from "../../../../src/audio/PlaybackClock"; import { PlaybackClock } from "../../../../src/audio/PlaybackClock";
import { mkEvent } from "../../../test-utils"; import { mkEvent } from "../../../test-utils";
import SettingsStore from "../../../../src/settings/SettingsStore";
jest.mock("../../../../src/utils/local-room", () => ({ jest.mock("../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(), doMaybeLocalRoomAction: jest.fn(),
@ -103,10 +102,6 @@ describe("<VoiceRecordComposerTile/>", () => {
return fn(roomId); return fn(roomId);
}, },
); );
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_intentional_mentions",
);
}); });
describe("send", () => { describe("send", () => {

View file

@ -15,18 +15,11 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { defer } from "matrix-js-sdk/src/utils";
import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab"; import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
import SettingsStore from "../../../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../../../src/settings/SettingsStore";
import {
getMockClientWithEventEmitter,
mockClientMethodsServer,
mockClientMethodsUser,
} from "../../../../../test-utils";
import SdkConfig from "../../../../../../src/SdkConfig"; import SdkConfig from "../../../../../../src/SdkConfig";
import MatrixClientBackedController from "../../../../../../src/settings/controllers/MatrixClientBackedController";
describe("<LabsUserSettingsTab />", () => { describe("<LabsUserSettingsTab />", () => {
const sdkConfigSpy = jest.spyOn(SdkConfig, "get"); const sdkConfigSpy = jest.spyOn(SdkConfig, "get");
@ -36,12 +29,6 @@ describe("<LabsUserSettingsTab />", () => {
}; };
const getComponent = () => <LabsUserSettingsTab {...defaultProps} />; const getComponent = () => <LabsUserSettingsTab {...defaultProps} />;
const userId = "@alice:server.org";
const cli = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
});
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
beforeEach(() => { beforeEach(() => {
@ -73,31 +60,4 @@ describe("<LabsUserSettingsTab />", () => {
const labsSections = container.getElementsByClassName("mx_SettingsSubsection"); const labsSections = container.getElementsByClassName("mx_SettingsSubsection");
expect(labsSections).toHaveLength(9); expect(labsSections).toHaveLength(9);
}); });
it("allow setting a labs flag which requires unstable support once support is confirmed", async () => {
// enable labs
sdkConfigSpy.mockImplementation((configName) => configName === "show_labs_settings");
const deferred = defer<boolean>();
cli.doesServerSupportUnstableFeature.mockImplementation(async (featureName) => {
return featureName === "org.matrix.msc3952_intentional_mentions" ? deferred.promise : false;
});
MatrixClientBackedController.matrixClient = cli;
const { queryByText } = render(getComponent());
expect(
queryByText("Enable intentional mentions")!
.closest(".mx_SettingsFlag")!
.querySelector(".mx_AccessibleButton"),
).toHaveAttribute("aria-disabled", "true");
deferred.resolve(true);
await waitFor(() => {
expect(
queryByText("Enable intentional mentions")!
.closest(".mx_SettingsFlag")!
.querySelector(".mx_AccessibleButton"),
).toHaveAttribute("aria-disabled", "false");
});
});
}); });