{
public static contextType = RoomContext;
- public render(): JSX.Element {
+ public render(): JSX.Element | null {
if (!this.props.replyToEvent) return null;
return
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 214db0000c..e22e6b7499 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -911,6 +911,7 @@
"Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)",
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
+ "Voice broadcast (under active development)": "Voice broadcast (under active development)",
"Use new session manager (under active development)": "Use new session manager (under active development)",
"Font size": "Font size",
"Use custom size": "Use custom size",
@@ -1813,6 +1814,7 @@
"Emoji": "Emoji",
"Hide stickers": "Hide stickers",
"Sticker": "Sticker",
+ "Voice broadcast": "Voice broadcast",
"Voice Message": "Voice Message",
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
"Poll": "Poll",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 1675968257..cb661b2169 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -101,6 +101,10 @@ export enum LabGroup {
Developer,
}
+export enum Features {
+ VoiceBroadcast = "feature_voice_broadcast",
+}
+
export const labGroupNames: Record = {
[LabGroup.Messaging]: _td("Messaging"),
[LabGroup.Profile]: _td("Profile"),
@@ -435,6 +439,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Favourite Messages (under active development)"),
default: false,
},
+ [Features.VoiceBroadcast]: {
+ isFeature: true,
+ labsGroup: LabGroup.Messaging,
+ supportedLevels: LEVELS_FEATURE,
+ displayName: _td("Voice broadcast (under active development)"),
+ default: false,
+ },
"feature_new_device_manager": {
isFeature: true,
labsGroup: LabGroup.Experimental,
diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx
index 2aa07fbeef..aa4245cebc 100644
--- a/test/components/views/rooms/MessageComposer-test.tsx
+++ b/test/components/views/rooms/MessageComposer-test.tsx
@@ -17,8 +17,8 @@ limitations under the License.
import * as React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
-import { RoomMember } from "matrix-js-sdk/src/models/room-member";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { MatrixEvent, MsgType, RoomMember } from "matrix-js-sdk/src/matrix";
+import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../test-utils";
import MessageComposer from "../../../../src/components/views/rooms/MessageComposer";
@@ -30,6 +30,15 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { LocalRoom } from "../../../../src/models/LocalRoom";
import MessageComposerButtons from "../../../../src/components/views/rooms/MessageComposerButtons";
+import { Features } from "../../../../src/settings/Settings";
+import SettingsStore from "../../../../src/settings/SettingsStore";
+import { SettingLevel } from "../../../../src/settings/SettingLevel";
+import dis from "../../../../src/dispatcher/dispatcher";
+import { Action } from "../../../../src/dispatcher/actions";
+import { SendMessageComposer } from "../../../../src/components/views/rooms/SendMessageComposer";
+import { E2EStatus } from "../../../../src/utils/ShieldUtils";
+import { addTextToComposer } from "../../../test-utils/composer";
+import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
describe("MessageComposer", () => {
stubClient();
@@ -54,7 +63,7 @@ describe("MessageComposer", () => {
});
it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", () => {
- const wrapper = wrapAndRender({ room }, true, mkEvent({
+ const wrapper = wrapAndRender({ room }, true, false, mkEvent({
event: true,
type: "m.room.tombstone",
room: room.roomId,
@@ -68,10 +77,258 @@ describe("MessageComposer", () => {
expect(wrapper.find("MessageComposerButtons")).toHaveLength(0);
expect(wrapper.find(".mx_MessageComposer_roomReplaced_header")).toHaveLength(1);
});
+
+ describe("when receiving a »reply_to_event«", () => {
+ let wrapper: ReactWrapper;
+ let resizeNotifier: ResizeNotifier;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ resizeNotifier = {
+ notifyTimelineHeightChanged: jest.fn(),
+ } as unknown as ResizeNotifier;
+ wrapper = wrapAndRender({
+ room,
+ resizeNotifier,
+ });
+ });
+
+ it("should call notifyTimelineHeightChanged() for the same context", () => {
+ dis.dispatch({
+ action: "reply_to_event",
+ context: (wrapper.instance as unknown as MessageComposer).context,
+ });
+ wrapper.update();
+
+ jest.advanceTimersByTime(150);
+ expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalled();
+ });
+
+ it("should not call notifyTimelineHeightChanged() for a different context", () => {
+ dis.dispatch({
+ action: "reply_to_event",
+ context: "test",
+ });
+ wrapper.update();
+
+ jest.advanceTimersByTime(150);
+ expect(resizeNotifier.notifyTimelineHeightChanged).not.toHaveBeenCalled();
+ });
+ });
+
+ // test button display depending on settings
+ [
+ {
+ setting: "MessageComposerInput.showStickersButton",
+ prop: "showStickersButton",
+ },
+ {
+ setting: "MessageComposerInput.showPollsButton",
+ prop: "showPollsButton",
+ },
+ {
+ setting: Features.VoiceBroadcast,
+ prop: "showVoiceBroadcastButton",
+ },
+ ].forEach(({ setting, prop }) => {
+ [true, false].forEach((value: boolean) => {
+ describe(`when ${setting} = ${value}`, () => {
+ let wrapper: ReactWrapper;
+
+ beforeEach(() => {
+ SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
+ wrapper = wrapAndRender({ room });
+ });
+
+ it(`should pass the prop ${prop} = ${value}`, () => {
+ expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(value);
+ });
+
+ describe(`and setting ${setting} to ${!value}`, () => {
+ beforeEach(async () => {
+ // simulate settings update
+ await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value);
+ dis.dispatch({
+ action: Action.SettingUpdated,
+ settingName: setting,
+ newValue: !value,
+ }, true);
+ wrapper.update();
+ });
+
+ it(`should pass the prop ${prop} = ${!value}`, () => {
+ expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(!value);
+ });
+ });
+ });
+ });
+ });
+
+ it("should not render the send button", () => {
+ const wrapper = wrapAndRender({ room });
+ expect(wrapper.find("SendButton")).toHaveLength(0);
+ });
+
+ describe("when a message has been entered", () => {
+ let wrapper: ReactWrapper;
+
+ beforeEach(() => {
+ wrapper = wrapAndRender({ room });
+ addTextToComposer(wrapper, "Hello");
+ wrapper.update();
+ });
+
+ it("should render the send button", () => {
+ expect(wrapper.find("SendButton")).toHaveLength(1);
+ });
+ });
+
+ describe("UIStore interactions", () => {
+ let wrapper: ReactWrapper;
+ let resizeCallback: Function;
+
+ beforeEach(() => {
+ jest.spyOn(UIStore.instance, "on").mockImplementation((_event: string, listener: Function): any => {
+ resizeCallback = listener;
+ });
+ });
+
+ describe("when a non-resize event occurred in UIStore", () => {
+ let stateBefore: any;
+
+ beforeEach(() => {
+ wrapper = wrapAndRender({ room });
+ stateBefore = { ...wrapper.instance().state };
+ resizeCallback("test", {});
+ wrapper.update();
+ });
+
+ it("should not change the state", () => {
+ expect(wrapper.instance().state).toEqual(stateBefore);
+ });
+ });
+
+ describe("when a resize to narrow event occurred in UIStore", () => {
+ beforeEach(() => {
+ wrapper = wrapAndRender({ room }, true, true);
+ wrapper.setState({
+ isMenuOpen: true,
+ isStickerPickerOpen: true,
+ });
+ resizeCallback(UI_EVENTS.Resize, {});
+ wrapper.update();
+ });
+
+ it("isMenuOpen should be true", () => {
+ expect(wrapper.state("isMenuOpen")).toBe(true);
+ });
+
+ it("isStickerPickerOpen should be false", () => {
+ expect(wrapper.state("isStickerPickerOpen")).toBe(false);
+ });
+ });
+
+ describe("when a resize to non-narrow event occurred in UIStore", () => {
+ beforeEach(() => {
+ wrapper = wrapAndRender({ room }, true, false);
+ wrapper.setState({
+ isMenuOpen: true,
+ isStickerPickerOpen: true,
+ });
+ resizeCallback(UI_EVENTS.Resize, {});
+ wrapper.update();
+ });
+
+ it("isMenuOpen should be false", () => {
+ expect(wrapper.state("isMenuOpen")).toBe(false);
+ });
+
+ it("isStickerPickerOpen should be false", () => {
+ expect(wrapper.state("isStickerPickerOpen")).toBe(false);
+ });
+ });
+ });
+
+ describe("when not replying to an event", () => {
+ it("should pass the expected placeholder to SendMessageComposer", () => {
+ const wrapper = wrapAndRender({ room });
+ expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send a message…");
+ });
+
+ it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => {
+ const wrapper = wrapAndRender({
+ room,
+ e2eStatus: E2EStatus.Normal,
+ });
+ expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send an encrypted message…");
+ });
+ });
+
+ describe("when replying to an event", () => {
+ let replyToEvent: MatrixEvent;
+ let props: Partial>;
+
+ const checkPlaceholder = (expected: string) => {
+ it("should pass the expected placeholder to SendMessageComposer", () => {
+ const wrapper = wrapAndRender(props);
+ expect(wrapper.find(SendMessageComposer).props().placeholder).toBe(expected);
+ });
+ };
+
+ const setEncrypted = () => {
+ beforeEach(() => {
+ props.e2eStatus = E2EStatus.Normal;
+ });
+ };
+
+ beforeEach(() => {
+ replyToEvent = mkEvent({
+ event: true,
+ type: MsgType.Text,
+ user: cli.getUserId(),
+ content: {},
+ });
+
+ props = {
+ room,
+ replyToEvent,
+ };
+ });
+
+ describe("without encryption", () => {
+ checkPlaceholder("Send a reply…");
+ });
+
+ describe("with encryption", () => {
+ setEncrypted();
+ checkPlaceholder("Send an encrypted reply…");
+ });
+
+ describe("with a non-thread relation", () => {
+ beforeEach(() => {
+ props.relation = { rel_type: "test" };
+ });
+
+ checkPlaceholder("Send a reply…");
+ });
+
+ describe("that is a thread", () => {
+ beforeEach(() => {
+ props.relation = { rel_type: THREAD_RELATION_TYPE.name };
+ });
+
+ checkPlaceholder("Reply to thread…");
+
+ describe("with encryption", () => {
+ setEncrypted();
+ checkPlaceholder("Reply to encrypted thread…");
+ });
+ });
+ });
});
describe("for a LocalRoom", () => {
- const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId());
+ const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!);
it("should pass the sticker picker disabled prop", () => {
const wrapper = wrapAndRender({ room: localRoom });
@@ -83,6 +340,7 @@ describe("MessageComposer", () => {
function wrapAndRender(
props: Partial> = {},
canSendMessages = true,
+ narrow = false,
tombstone?: MatrixEvent,
): ReactWrapper {
const mockClient = MatrixClientPeg.get();
@@ -97,7 +355,10 @@ function wrapAndRender(
};
const roomState = {
- room, canSendMessages, tombstone,
+ room,
+ canSendMessages,
+ tombstone,
+ narrow,
} as unknown as IRoomState;
const defaultProps = {
diff --git a/test/components/views/rooms/MessageComposerButtons-test.tsx b/test/components/views/rooms/MessageComposerButtons-test.tsx
index 6666a03f1f..ade14f752e 100644
--- a/test/components/views/rooms/MessageComposerButtons-test.tsx
+++ b/test/components/views/rooms/MessageComposerButtons-test.tsx
@@ -34,7 +34,7 @@ const mockProps: React.ComponentProps = {
addEmoji: () => false,
haveRecording: false,
isStickerPickerOpen: false,
- menuPosition: null,
+ menuPosition: undefined,
onRecordStartEndClick: () => {},
setStickerPickerOpen: () => {},
toggleButtonMenu: () => {},
@@ -44,11 +44,11 @@ describe("MessageComposerButtons", () => {
it("Renders emoji and upload buttons in wide mode", () => {
const buttons = wrapAndRender(
,
false,
);
@@ -63,11 +63,11 @@ describe("MessageComposerButtons", () => {
it("Renders other buttons in menu in wide mode", () => {
const buttons = wrapAndRender(
,
false,
);
@@ -88,11 +88,11 @@ describe("MessageComposerButtons", () => {
it("Renders only some buttons in narrow mode", () => {
const buttons = wrapAndRender(
,
true,
);
@@ -106,11 +106,11 @@ describe("MessageComposerButtons", () => {
it("Renders other buttons in menu (except voice messages) in narrow mode", () => {
const buttons = wrapAndRender(
,
true,
);
@@ -131,11 +131,11 @@ describe("MessageComposerButtons", () => {
it('should render when asked to', () => {
const buttons = wrapAndRender(
,
true,
);
@@ -155,11 +155,11 @@ describe("MessageComposerButtons", () => {
it('should not render when asked not to', () => {
const buttons = wrapAndRender(
,
true,
);
@@ -176,6 +176,35 @@ describe("MessageComposerButtons", () => {
]);
});
});
+
+ describe("with showVoiceBroadcastButton = true", () => {
+ it("should render the »Voice broadcast« button", () => {
+ const buttons = wrapAndRender(
+ ,
+ false,
+ );
+
+ expect(buttonLabels(buttons)).toEqual([
+ "Emoji",
+ "Attachment",
+ "More options",
+ [
+ "Sticker",
+ "Voice Message",
+ "Voice broadcast",
+ "Poll",
+ "Location",
+ ],
+ ]);
+ });
+ });
});
function wrapAndRender(component: React.ReactElement, narrow: boolean): ReactWrapper {
diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx
index cb10032720..6f6f846c24 100644
--- a/test/components/views/rooms/SendMessageComposer-test.tsx
+++ b/test/components/views/rooms/SendMessageComposer-test.tsx
@@ -40,6 +40,7 @@ import { IRoomState } from "../../../../src/components/structures/RoomView";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { mockPlatformPeg } from "../../../test-utils/platform";
import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room";
+import { addTextToComposer } from "../../../test-utils/composer";
jest.mock("../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(),
@@ -187,20 +188,6 @@ describe('', () => {
spyDispatcher.mockReset();
});
- const addTextToComposer = (wrapper, text) => act(() => {
- // couldn't get input event on contenteditable to work
- // paste works without illegal private method access
- const pasteEvent = {
- clipboardData: {
- types: [],
- files: [],
- getData: type => type === "text/plain" ? text : undefined,
- },
- };
- wrapper.find('[role="textbox"]').simulate('paste', pasteEvent);
- wrapper.update();
- });
-
const defaultProps = {
room: mockRoom,
toggleStickerPickerOpen: jest.fn(),
diff --git a/test/test-utils/composer.ts b/test/test-utils/composer.ts
new file mode 100644
index 0000000000..abfb694d96
--- /dev/null
+++ b/test/test-utils/composer.ts
@@ -0,0 +1,33 @@
+/*
+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.
+*/
+
+// eslint-disable-next-line deprecate/import
+import { ReactWrapper } from "enzyme";
+import { act } from "react-dom/test-utils";
+
+export const addTextToComposer = (wrapper: ReactWrapper, text: string) => act(() => {
+ // couldn't get input event on contenteditable to work
+ // paste works without illegal private method access
+ const pasteEvent = {
+ clipboardData: {
+ types: [],
+ files: [],
+ getData: type => type === "text/plain" ? text : undefined,
+ },
+ };
+ wrapper.find('[role="textbox"]').simulate('paste', pasteEvent);
+ wrapper.update();
+});