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

Allow pressing Enter to send messages in new composer #9451

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions cypress/e2e/composer/composer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/// <reference types="cypress" />

import { SynapseInstance } from "../../plugins/synapsedocker";
import { SettingLevel } from "../../../src/settings/SettingLevel";

describe("Composer", () => {
let synapse: SynapseInstance;

beforeEach(() => {
cy.startSynapse("default").then(data => {
synapse = data;
});
});

afterEach(() => {
cy.stopSynapse(synapse);
});

describe("CIDER", () => {
beforeEach(() => {
cy.initTestUser(synapse, "Janet").then(() => {
cy.createRoom({ name: "Composing Room" });
});
cy.viewRoomByName("Composing Room");
});

it("sends a message when you click send or press Enter", () => {
// Type a message
cy.get('div[contenteditable=true]').type('my message 0');
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');

// Click send
cy.get('div[aria-label="Send message"]').click();
// It has been sent
cy.contains('.mx_EventTile_body', 'my message 0');

// Type another and press Enter afterwards
cy.get('div[contenteditable=true]').type('my message 1{enter}');
// It was sent
cy.contains('.mx_EventTile_body', 'my message 1');
});

it("can write formatted text", () => {
cy.get('div[contenteditable=true]').type('my bold{ctrl+b} message');
cy.get('div[aria-label="Send message"]').click();
// Note: both "bold" and "message" are bold, which is probably surprising
cy.contains('.mx_EventTile_body strong', 'bold message');
});

describe("when Ctrl+Enter is required to send", () => {
beforeEach(() => {
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
});

it("only sends when you press Ctrl+Enter", () => {
// Type a message and press Enter
cy.get('div[contenteditable=true]').type('my message 3{enter}');
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');

// Press Ctrl+Enter
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
// It was sent
cy.contains('.mx_EventTile_body', 'my message 3');
});
});
});

describe("WYSIWYG", () => {
beforeEach(() => {
cy.enableLabsFeature("feature_wysiwyg_composer");
cy.initTestUser(synapse, "Janet").then(() => {
cy.createRoom({ name: "Composing Room" });
});
cy.viewRoomByName("Composing Room");
});

it("sends a message when you click send or press Enter", () => {
// Type a message
cy.get('div[contenteditable=true]').type('my message 0');
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');

// Click send
cy.get('div[aria-label="Send message"]').click();
// It has been sent
cy.contains('.mx_EventTile_body', 'my message 0');

// Type another
cy.get('div[contenteditable=true]').type('my message 1');
// Press enter. Would be nice to just use {enter} but we can't because Cypress
// does not trigger an insertParagraph when you do that.
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
// It was sent
cy.contains('.mx_EventTile_body', 'my message 1');
});

it("can write formatted text", () => {
cy.get('div[contenteditable=true]').type('my {ctrl+b}bold{ctrl+b} message');
cy.get('div[aria-label="Send message"]').click();
cy.contains('.mx_EventTile_body strong', 'bold');
});

describe("when Ctrl+Enter is required to send", () => {
beforeEach(() => {
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
});

it("only sends when you press Ctrl+Enter", () => {
// Type a message and press Enter
cy.get('div[contenteditable=true]').type('my message 3');
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');

// Press Ctrl+Enter
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
// It was sent
cy.contains('.mx_EventTile_body', 'my message 3');
});
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.2.0",
"@matrix-org/matrix-wysiwyg": "^0.2.0",
"@matrix-org/matrix-wysiwyg": "^0.3.0",
"@matrix-org/react-sdk-module-api": "^0.0.3",
"@sentry/browser": "^6.11.0",
"@sentry/tracing": "^6.11.0",
Expand Down
24 changes: 22 additions & 2 deletions src/components/views/rooms/wysiwyg_composer/WysiwygComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

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

import { Editor } from './Editor';
import { FormattingButtons } from './FormattingButtons';
Expand All @@ -25,6 +25,7 @@ import { sendMessage } from './message';
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
import { useRoomContext } from '../../../../contexts/RoomContext';
import { useWysiwygActionHandler } from './useWysiwygActionHandler';
import { useSettingValue } from '../../../../hooks/useSettings';

interface WysiwygProps {
disabled?: boolean;
Expand All @@ -41,8 +42,27 @@ export function WysiwygComposer(
) {
const roomContext = useRoomContext();
const mxClient = useMatrixClientContext();
const ctrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");

const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg();
function inputEventProcessor(event: WysiwygInputEvent, wysiwyg: Wysiwyg): WysiwygInputEvent | null {
if (event instanceof ClipboardEvent) {
return event;
}

if (
(event.inputType === 'insertParagraph' && !ctrlEnterToSend) ||
event.inputType === 'sendMessage'
) {
sendMessage(content, { mxClient, roomContext, ...props });
wysiwyg.actions.clear();
ref.current?.focus();
return null;
}

return event;
}

const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ inputEventProcessor });

useEffect(() => {
if (!disabled && content !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
import "@testing-library/jest-dom";
import React from "react";
import { act, render, screen, waitFor } from "@testing-library/react";
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";

import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
Expand All @@ -26,13 +27,31 @@ import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
import SettingsStore from "../../../../../src/settings/SettingsStore";

// Work around missing ClipboardEvent type
class MyClipbardEvent {}
window.ClipboardEvent = MyClipbardEvent as any;

let inputEventProcessor: InputEventProcessor | null = null;

// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
jest.mock("@matrix-org/matrix-wysiwyg", () => ({
useWysiwyg: () => {
return { ref: { current: null }, content: '<b>html</b>', isWysiwygReady: true, wysiwyg: { clear: () => void 0 },
formattingStates: { bold: 'enabled', italic: 'enabled', underline: 'enabled', strikeThrough: 'enabled' } };
useWysiwyg: (props: WysiwygProps) => {
inputEventProcessor = props.inputEventProcessor ?? null;
return {
ref: { current: null },
content: '<b>html</b>',
isWysiwygReady: true,
wysiwyg: { clear: () => void 0 },
formattingStates: {
bold: 'enabled',
italic: 'enabled',
underline: 'enabled',
strikeThrough: 'enabled',
},
};
},
}));

Expand Down Expand Up @@ -196,5 +215,62 @@ describe('WysiwygComposer', () => {
// Then we don't get it because we are disabled
expect(screen.getByRole('textbox')).not.toHaveFocus();
});

it('sends a message when Enter is pressed', async () => {
// Given a composer
customRender(() => {}, false);

// When we tell its inputEventProcesser that the user pressed Enter
const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" });
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
inputEventProcessor(event, wysiwyg);

// Then it sends a message
expect(mockClient.sendMessage).toBeCalledWith(
"myfakeroom",
null,
{
"body": "<b>html</b>",
"format": "org.matrix.custom.html",
"formatted_body": "<b>html</b>",
"msgtype": "m.text",
},
);
// TODO: plain text body above is wrong - will be fixed when we provide markdown for it
});

describe('when settings require Ctrl+Enter to send', () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "MessageComposerInput.ctrlEnterToSend") return true;
});
});

it('does not send a message when Enter is pressed', async () => {
// Given a composer
customRender(() => {}, false);

// When we tell its inputEventProcesser that the user pressed Enter
const event = new InputEvent("input", { inputType: "insertParagraph" });
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
inputEventProcessor(event, wysiwyg);

// Then it does not send a message
expect(mockClient.sendMessage).toBeCalledTimes(0);
});

it('sends a message when Ctrl+Enter is pressed', async () => {
// Given a composer
customRender(() => {}, false);

// When we tell its inputEventProcesser that the user pressed Ctrl+Enter
const event = new InputEvent("input", { inputType: "sendMessage" });
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
inputEventProcessor(event, wysiwyg);

// Then it sends a message
expect(mockClient.sendMessage).toBeCalledTimes(1);
});
});
});

Loading