Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit e0ab0ac

Browse files
authored
Allow pressing Enter to send messages in new composer (#9451)
* Allow pressing Enter to send messages in new composer * Cypress tests for composer send behaviour
1 parent 26f3d10 commit e0ab0ac

File tree

5 files changed

+323
-12
lines changed

5 files changed

+323
-12
lines changed

cypress/e2e/composer/composer.spec.ts

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/// <reference types="cypress" />
18+
19+
import { SynapseInstance } from "../../plugins/synapsedocker";
20+
import { SettingLevel } from "../../../src/settings/SettingLevel";
21+
22+
describe("Composer", () => {
23+
let synapse: SynapseInstance;
24+
25+
beforeEach(() => {
26+
cy.startSynapse("default").then(data => {
27+
synapse = data;
28+
});
29+
});
30+
31+
afterEach(() => {
32+
cy.stopSynapse(synapse);
33+
});
34+
35+
describe("CIDER", () => {
36+
beforeEach(() => {
37+
cy.initTestUser(synapse, "Janet").then(() => {
38+
cy.createRoom({ name: "Composing Room" });
39+
});
40+
cy.viewRoomByName("Composing Room");
41+
});
42+
43+
it("sends a message when you click send or press Enter", () => {
44+
// Type a message
45+
cy.get('div[contenteditable=true]').type('my message 0');
46+
// It has not been sent yet
47+
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
48+
49+
// Click send
50+
cy.get('div[aria-label="Send message"]').click();
51+
// It has been sent
52+
cy.contains('.mx_EventTile_body', 'my message 0');
53+
54+
// Type another and press Enter afterwards
55+
cy.get('div[contenteditable=true]').type('my message 1{enter}');
56+
// It was sent
57+
cy.contains('.mx_EventTile_body', 'my message 1');
58+
});
59+
60+
it("can write formatted text", () => {
61+
cy.get('div[contenteditable=true]').type('my bold{ctrl+b} message');
62+
cy.get('div[aria-label="Send message"]').click();
63+
// Note: both "bold" and "message" are bold, which is probably surprising
64+
cy.contains('.mx_EventTile_body strong', 'bold message');
65+
});
66+
67+
describe("when Ctrl+Enter is required to send", () => {
68+
beforeEach(() => {
69+
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
70+
});
71+
72+
it("only sends when you press Ctrl+Enter", () => {
73+
// Type a message and press Enter
74+
cy.get('div[contenteditable=true]').type('my message 3{enter}');
75+
// It has not been sent yet
76+
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
77+
78+
// Press Ctrl+Enter
79+
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
80+
// It was sent
81+
cy.contains('.mx_EventTile_body', 'my message 3');
82+
});
83+
});
84+
});
85+
86+
describe("WYSIWYG", () => {
87+
beforeEach(() => {
88+
cy.enableLabsFeature("feature_wysiwyg_composer");
89+
cy.initTestUser(synapse, "Janet").then(() => {
90+
cy.createRoom({ name: "Composing Room" });
91+
});
92+
cy.viewRoomByName("Composing Room");
93+
});
94+
95+
it("sends a message when you click send or press Enter", () => {
96+
// Type a message
97+
cy.get('div[contenteditable=true]').type('my message 0');
98+
// It has not been sent yet
99+
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
100+
101+
// Click send
102+
cy.get('div[aria-label="Send message"]').click();
103+
// It has been sent
104+
cy.contains('.mx_EventTile_body', 'my message 0');
105+
106+
// Type another
107+
cy.get('div[contenteditable=true]').type('my message 1');
108+
// Press enter. Would be nice to just use {enter} but we can't because Cypress
109+
// does not trigger an insertParagraph when you do that.
110+
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
111+
// It was sent
112+
cy.contains('.mx_EventTile_body', 'my message 1');
113+
});
114+
115+
it("can write formatted text", () => {
116+
cy.get('div[contenteditable=true]').type('my {ctrl+b}bold{ctrl+b} message');
117+
cy.get('div[aria-label="Send message"]').click();
118+
cy.contains('.mx_EventTile_body strong', 'bold');
119+
});
120+
121+
describe("when Ctrl+Enter is required to send", () => {
122+
beforeEach(() => {
123+
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
124+
});
125+
126+
it("only sends when you press Ctrl+Enter", () => {
127+
// Type a message and press Enter
128+
cy.get('div[contenteditable=true]').type('my message 3');
129+
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
130+
// It has not been sent yet
131+
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
132+
133+
// Press Ctrl+Enter
134+
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
135+
// It was sent
136+
cy.contains('.mx_EventTile_body', 'my message 3');
137+
});
138+
});
139+
});
140+
});

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"dependencies": {
5858
"@babel/runtime": "^7.12.5",
5959
"@matrix-org/analytics-events": "^0.2.0",
60-
"@matrix-org/matrix-wysiwyg": "^0.2.0",
60+
"@matrix-org/matrix-wysiwyg": "^0.3.0",
6161
"@matrix-org/react-sdk-module-api": "^0.0.3",
6262
"@sentry/browser": "^6.11.0",
6363
"@sentry/tracing": "^6.11.0",

src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
import React, { useCallback, useEffect } from 'react';
1818
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
19-
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
19+
import { useWysiwyg, Wysiwyg, WysiwygInputEvent } from "@matrix-org/matrix-wysiwyg";
2020

2121
import { Editor } from './Editor';
2222
import { FormattingButtons } from './FormattingButtons';
@@ -25,6 +25,7 @@ import { sendMessage } from './message';
2525
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
2626
import { useRoomContext } from '../../../../contexts/RoomContext';
2727
import { useWysiwygActionHandler } from './useWysiwygActionHandler';
28+
import { useSettingValue } from '../../../../hooks/useSettings';
2829

2930
interface WysiwygProps {
3031
disabled?: boolean;
@@ -41,8 +42,27 @@ export function WysiwygComposer(
4142
) {
4243
const roomContext = useRoomContext();
4344
const mxClient = useMatrixClientContext();
45+
const ctrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");
4446

45-
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg();
47+
function inputEventProcessor(event: WysiwygInputEvent, wysiwyg: Wysiwyg): WysiwygInputEvent | null {
48+
if (event instanceof ClipboardEvent) {
49+
return event;
50+
}
51+
52+
if (
53+
(event.inputType === 'insertParagraph' && !ctrlEnterToSend) ||
54+
event.inputType === 'sendMessage'
55+
) {
56+
sendMessage(content, { mxClient, roomContext, ...props });
57+
wysiwyg.actions.clear();
58+
ref.current?.focus();
59+
return null;
60+
}
61+
62+
return event;
63+
}
64+
65+
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ inputEventProcessor });
4666

4767
useEffect(() => {
4868
if (!disabled && content !== null) {

test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx

+79-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
import "@testing-library/jest-dom";
1818
import React from "react";
1919
import { act, render, screen, waitFor } from "@testing-library/react";
20+
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
2021

2122
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
2223
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
@@ -26,13 +27,31 @@ import { IRoomState } from "../../../../../src/components/structures/RoomView";
2627
import { Layout } from "../../../../../src/settings/enums/Layout";
2728
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
2829
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
30+
import SettingsStore from "../../../../../src/settings/SettingsStore";
31+
32+
// Work around missing ClipboardEvent type
33+
class MyClipbardEvent {}
34+
window.ClipboardEvent = MyClipbardEvent as any;
35+
36+
let inputEventProcessor: InputEventProcessor | null = null;
2937

3038
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
3139
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
3240
jest.mock("@matrix-org/matrix-wysiwyg", () => ({
33-
useWysiwyg: () => {
34-
return { ref: { current: null }, content: '<b>html</b>', isWysiwygReady: true, wysiwyg: { clear: () => void 0 },
35-
formattingStates: { bold: 'enabled', italic: 'enabled', underline: 'enabled', strikeThrough: 'enabled' } };
41+
useWysiwyg: (props: WysiwygProps) => {
42+
inputEventProcessor = props.inputEventProcessor ?? null;
43+
return {
44+
ref: { current: null },
45+
content: '<b>html</b>',
46+
isWysiwygReady: true,
47+
wysiwyg: { clear: () => void 0 },
48+
formattingStates: {
49+
bold: 'enabled',
50+
italic: 'enabled',
51+
underline: 'enabled',
52+
strikeThrough: 'enabled',
53+
},
54+
};
3655
},
3756
}));
3857

@@ -196,5 +215,62 @@ describe('WysiwygComposer', () => {
196215
// Then we don't get it because we are disabled
197216
expect(screen.getByRole('textbox')).not.toHaveFocus();
198217
});
218+
219+
it('sends a message when Enter is pressed', async () => {
220+
// Given a composer
221+
customRender(() => {}, false);
222+
223+
// When we tell its inputEventProcesser that the user pressed Enter
224+
const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" });
225+
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
226+
inputEventProcessor(event, wysiwyg);
227+
228+
// Then it sends a message
229+
expect(mockClient.sendMessage).toBeCalledWith(
230+
"myfakeroom",
231+
null,
232+
{
233+
"body": "<b>html</b>",
234+
"format": "org.matrix.custom.html",
235+
"formatted_body": "<b>html</b>",
236+
"msgtype": "m.text",
237+
},
238+
);
239+
// TODO: plain text body above is wrong - will be fixed when we provide markdown for it
240+
});
241+
242+
describe('when settings require Ctrl+Enter to send', () => {
243+
beforeEach(() => {
244+
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
245+
if (name === "MessageComposerInput.ctrlEnterToSend") return true;
246+
});
247+
});
248+
249+
it('does not send a message when Enter is pressed', async () => {
250+
// Given a composer
251+
customRender(() => {}, false);
252+
253+
// When we tell its inputEventProcesser that the user pressed Enter
254+
const event = new InputEvent("input", { inputType: "insertParagraph" });
255+
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
256+
inputEventProcessor(event, wysiwyg);
257+
258+
// Then it does not send a message
259+
expect(mockClient.sendMessage).toBeCalledTimes(0);
260+
});
261+
262+
it('sends a message when Ctrl+Enter is pressed', async () => {
263+
// Given a composer
264+
customRender(() => {}, false);
265+
266+
// When we tell its inputEventProcesser that the user pressed Ctrl+Enter
267+
const event = new InputEvent("input", { inputType: "sendMessage" });
268+
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
269+
inputEventProcessor(event, wysiwyg);
270+
271+
// Then it sends a message
272+
expect(mockClient.sendMessage).toBeCalledTimes(1);
273+
});
274+
});
199275
});
200276

0 commit comments

Comments
 (0)