Add Voice Broadcast labs setting and composer button (#9279)

* Add Voice Broadcast labs setting and composer button

* Implement strict typing

* Extend MessageComposer-test

* Extend tests

* Revert some strict type fixex

* Convert FEATURES to enum; change case

* Use fake timers in MessageComposer-test
pull/28788/head^2
Michael Weimann 2022-09-16 11:10:33 +02:00 committed by GitHub
parent 4a23630e06
commit a0c35d088a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 469 additions and 45 deletions

View File

@ -245,6 +245,10 @@ limitations under the License.
mask-image: url('$(res)/img/voip/mic-on-mask.svg');
}
.mx_MessageComposer_voiceBroadcast::before {
mask-image: url('$(res)/img/element-icons/live.svg');
}
.mx_MessageComposer_emoji::before {
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
}

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="21.799"
height="21.799"
viewBox="0 0 21.799 21.799"
fill="none"
version="1.1"
id="svg12"
sodipodi:docname="live.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="35.416667"
inkscape:cx="8.7670588"
inkscape:cy="8.1882353"
inkscape:window-width="1920"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<path
d="m 19.188575,3.0855891 c -0.3391,-0.43594 -0.9674,-0.51448 -1.4033,-0.17541 -0.4355,0.3387 -0.5143,0.96596 -0.1766,1.40185 l 8e-4,9.8e-4 0.001,0.00129 0.0136,0.01816 c 0.0133,0.0178 0.0346,0.04686 0.0629,0.0867 0.0566,0.07972 0.1408,0.20227 0.2435,0.36368 0.2058,0.32333 0.4838,0.79947 0.7625,1.39672 0.5603,1.20054 1.1062,2.85339 1.1062,4.7199899 0,1.8666 -0.5459,3.5194 -1.1062,4.72 -0.2787,0.5972 -0.5567,1.0733 -0.7625,1.3967 -0.1027,0.1614 -0.1869,0.2839 -0.2435,0.3637 -0.0283,0.0398 -0.0496,0.0689 -0.0629,0.0867 l -0.0136,0.0181 -0.001,0.0013 -9e-4,0.0012 c -0.3376,0.4358 -0.2587,1.063 0.1767,1.4016 0.4359,0.3391 1.0642,0.2606 1.4033,-0.1754 l -0.7453,-0.5796 c 0.7453,0.5796 0.7453,0.5796 0.7453,0.5796 l 0.002,-0.0025 0.0028,-0.0038 0.0083,-0.0108 0.0265,-0.0352 c 0.0219,-0.0294 0.0521,-0.0707 0.0895,-0.1232 0.0746,-0.1051 0.1779,-0.2558 0.3002,-0.448 0.2442,-0.3838 0.5662,-0.9362 0.8875,-1.6247 0.6397,-1.3709 1.2938,-3.318 1.2938,-5.5657 0,-2.2477199 -0.6541,-4.1948699 -1.2938,-5.5657599 -0.3213,-0.68846 -0.6433,-1.2409 -0.8875,-1.6247 -0.1223,-0.19217 -0.2256,-0.34283 -0.3002,-0.44793 -0.0374,-0.05258 -0.0676,-0.09382 -0.0895,-0.12323 l -0.0265,-0.03521 -0.0083,-0.01085 -0.0028,-0.00371 -0.0012,-0.00143 c 0,0 -8e-4,-0.00114 -0.7902,0.6128 z"
fill="#c1c6cd"
id="path2" />
<path
d="m 16.589375,6.6858091 c -0.339,-0.43595 -0.9673,-0.51448 -1.4033,-0.17541 -0.4348,0.33819 -0.514,0.96407 -0.178,1.39987 l 0.0034,0.00458 c 0.0045,0.00599 0.0129,0.01748 0.0248,0.03422 0.0238,0.03351 0.0612,0.08776 0.1076,0.16077 0.0933,0.14655 0.2213,0.36554 0.35,0.64137 0.2603,0.55764 0.5062,1.3105399 0.5062,2.1485399 0,0.838 -0.2459,1.5909 -0.5062,2.1485 -0.1287,0.2759 -0.2567,0.4949 -0.35,0.6414 -0.0464,0.073 -0.0838,0.1273 -0.1076,0.1608 -0.0119,0.0167 -0.0203,0.0282 -0.0248,0.0342 l -0.0034,0.0046 c -0.336,0.4358 -0.2568,1.0617 0.178,1.3999 0.436,0.339 1.0643,0.2605 1.4033,-0.1755 l -0.7893,-0.6139 c 0.7893,0.6139 0.7893,0.6139 0.7893,0.6139 l 0.0018,-0.0022 0.0022,-0.0029 0.0058,-0.0075 0.0164,-0.0219 c 0.0131,-0.0176 0.0305,-0.0412 0.0514,-0.0707 0.0418,-0.0589 0.0982,-0.1413 0.1643,-0.245 0.1317,-0.2071 0.3037,-0.5024 0.475,-0.8694 0.3397,-0.728 0.6938,-1.7752 0.6938,-2.9943 0,-1.2190999 -0.3541,-2.2662799 -0.6938,-2.9943099 -0.1713,-0.36703 -0.3433,-0.66233 -0.475,-0.86935 -0.0661,-0.10377 -0.1225,-0.18613 -0.1643,-0.24503 -0.0209,-0.02947 -0.0383,-0.05314 -0.0514,-0.07074 l -0.0164,-0.02187 -0.0058,-0.00748 -0.0022,-0.00287 -9e-4,-0.00122 c 0,0 -9e-4,-0.00107 -0.7902,0.61287 z"
fill="#c1c6cd"
id="path4" />
<path
d="m 2.6104749,18.713449 c 0.33907,0.4359 0.96734,0.5144 1.40329,0.1754 0.43547,-0.3387 0.5143,-0.966 0.17653,-1.4019 l -7.6e-4,-10e-4 -9.7e-4,-0.0013 -0.01365,-0.0181 c -0.01325,-0.0178 -0.0346,-0.0469 -0.06289,-0.0867 -0.05661,-0.0797 -0.14082,-0.2023 -0.24354,-0.3637 -0.20576,-0.3233 -0.48376,-0.7995 -0.76248,-1.3967 -0.56025,-1.2006 -1.10618,-2.8534 -1.10618,-4.72 0,-1.8665999 0.54593,-3.5194099 1.10618,-4.7199499 0.27872,-0.59726 0.55672,-1.07339 0.76248,-1.39673 0.10272,-0.1614 0.18693,-0.28396 0.24354,-0.36368 0.02829,-0.03983 0.04964,-0.0689 0.06289,-0.0867 l 0.01365,-0.01815 9.7e-4,-0.00129 9.1e-4,-0.00117 c 0.33761,-0.43588 0.25873,-1.06302 -0.17668,-1.40166 -0.43595,-0.33907 -1.06422,-0.26054 -1.40329,0.17541 l 0.74526,0.57964 c -0.74527,-0.57963 -0.74526,-0.57964 -0.74526,-0.57964 l -0.00199,0.00256 -0.00287,0.00372 -0.0083,0.01085 -0.02649,0.0352 c -0.0219,0.02941 -0.05212,0.07066 -0.08945,0.12323 -0.07464,0.10511 -0.17792,0.25577 -0.30021,0.44793 -0.24424,0.38381 -0.56624,0.93625 -0.88752,1.62471 -0.63975001,1.37088 -1.29382000681,3.31804 -1.29382000681,5.5657199 0,2.2477 0.65406999681,4.1949 1.29382000681,5.5658 0.32128,0.6884 0.64328,1.2409 0.88752,1.6247 0.12229,0.1921 0.22557,0.3428 0.30021,0.4479 0.03733,0.0526 0.06755,0.0938 0.08945,0.1232 l 0.02649,0.0352 0.0083,0.0109 0.00287,0.0037 0.0011,0.0014 c 0,0 8.9e-4,0.0012 0.79024,-0.6128 z"
fill="#c1c6cd"
id="path6" />
<path
d="m 5.2095949,15.113149 c 0.33907,0.436 0.96735,0.5145 1.40329,0.1754 0.43481,-0.3381 0.51407,-0.964 0.17806,-1.3998 l -0.00343,-0.0046 c -0.00447,-0.006 -0.01292,-0.0175 -0.0248,-0.0342 -0.0238,-0.0335 -0.06114,-0.0878 -0.10761,-0.1608 -0.09326,-0.1465 -0.22126,-0.3655 -0.34997,-0.6414 -0.26026,-0.5576 -0.50619,-1.3105 -0.50619,-2.1485 0,-0.838 0.24593,-1.5908999 0.50619,-2.1485499 0.12871,-0.27582 0.25671,-0.49481 0.34997,-0.64136 0.04647,-0.07302 0.08381,-0.12727 0.10761,-0.16078 0.01188,-0.01673 0.02033,-0.02822 0.0248,-0.03422 l 0.00344,-0.00458 c 0.336,-0.43581 0.25674,-1.06168 -0.17807,-1.39986 -0.43594,-0.33907 -1.06422,-0.26054 -1.40329,0.17541 l 0.78935,0.61394 c -0.78935,-0.61394 -0.78935,-0.61394 -0.78935,-0.61394 l -0.00177,0.00228 -0.00222,0.00287 -0.00572,0.00749 -0.01646,0.02186 c -0.01311,0.01761 -0.03044,0.04128 -0.05137,0.07075 -0.04182,0.0589 -0.09823,0.14125 -0.16427,0.24503 -0.13174,0.20702 -0.30374,0.50231 -0.47502,0.86934 -0.33975,0.72803 -0.69382,1.77522 -0.69382,2.9943199 0,1.2191 0.35407,2.2663 0.69382,2.9943 0.17128,0.367 0.34328,0.6623 0.47502,0.8694 0.06604,0.1037 0.12245,0.1861 0.16427,0.245 0.02093,0.0295 0.03826,0.0531 0.05137,0.0707 l 0.01646,0.0219 0.00572,0.0075 0.00222,0.0029 9.4e-4,0.0012 c 0,0 8.3e-4,10e-4 0.79018,-0.6129 z"
fill="#c1c6cd"
id="path8" />
<circle
cx="10.999774"
cy="10.898949"
r="2"
fill="#c1c6cd"
id="circle10" />
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -103,7 +103,7 @@ export class ModalManager {
}
public createDialog<T extends any[]>(
Element: React.ComponentType,
Element: React.ComponentType<any>,
...rest: ParametersWithoutFirst<ModalManager["createDialogAsync"]>
) {
return this.createDialogAsync<T>(Promise.resolve(Element), ...rest);

View File

@ -28,7 +28,7 @@ import LocationShareMenu from './LocationShareMenu';
interface IProps {
roomId: string;
sender: RoomMember;
menuPosition: AboveLeftOf;
menuPosition?: AboveLeftOf;
relation?: IEventRelation;
}

View File

@ -52,6 +52,7 @@ import MessageComposerButtons from './MessageComposerButtons';
import { ButtonEvent } from '../elements/AccessibleButton';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
import { Features } from '../../../settings/Settings';
let instanceCount = 0;
@ -89,10 +90,11 @@ interface IState {
isStickerPickerOpen: boolean;
showStickersButton: boolean;
showPollsButton: boolean;
showVoiceBroadcastButton: boolean;
}
export default class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef: string;
private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
private ref: React.RefObject<HTMLDivElement> = createRef();
@ -114,17 +116,19 @@ export default class MessageComposer extends React.Component<IProps, IState> {
this.state = {
isComposerEmpty: true,
haveRecording: false,
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
isMenuOpen: false,
isStickerPickerOpen: false,
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
};
this.instanceId = instanceCount++;
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
}
private get voiceRecording(): Optional<VoiceRecording> {
@ -153,7 +157,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
public componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.waitForOwnMember();
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current);
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!);
UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize);
this.updateRecordingState(); // grab any cached recordings
}
@ -199,13 +203,19 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
break;
}
case Features.VoiceBroadcast: {
if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) {
this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue });
}
break;
}
}
}
}
};
private waitForOwnMember() {
// if we have the member already, do that
// If we have the member already, do that
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
if (me) {
this.setState({ me });
@ -242,6 +252,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
const viaServers = [this.context.tombstone.getSender().split(':').slice(1).join(':')];
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
highlighted: true,
@ -426,8 +437,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
let recordingTooltip;
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
if (secondsLeft) {
if (this.state.recordingTimeLeftSeconds) {
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
recordingTooltip = <Tooltip
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
alignment={Alignment.Top}
@ -484,6 +495,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
// Sends a voice message. To be replaced by voice broadcast during development.
this.voiceRecordingButton.current?.onRecordStartEndClick();
if (this.context.narrow) {
this.toggleButtonMenu();
}
}}
/> }
{ showSendButton && (
<SendButton

View File

@ -45,7 +45,7 @@ interface IProps {
haveRecording: boolean;
isMenuOpen: boolean;
isStickerPickerOpen: boolean;
menuPosition: AboveLeftOf;
menuPosition?: AboveLeftOf;
onRecordStartEndClick: () => void;
relation?: IEventRelation;
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
@ -53,6 +53,8 @@ interface IProps {
showPollsButton: boolean;
showStickersButton: boolean;
toggleButtonMenu: () => void;
showVoiceBroadcastButton: boolean;
onStartVoiceBroadcastClick: () => void;
}
type OverflowMenuCloser = () => void;
@ -76,7 +78,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
uploadButton(), // props passed via UploadButtonContext
showStickersButton(props),
voiceRecordingButton(props, narrow),
props.showPollsButton && pollButton(room, props.relation),
startVoiceBroadcastButton(props),
props.showPollsButton ? pollButton(room, props.relation) : null,
showLocationButton(props, room, roomId, matrixClient),
];
} else {
@ -87,7 +90,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
moreButtons = [
showStickersButton(props),
voiceRecordingButton(props, narrow),
props.showPollsButton && pollButton(room, props.relation),
startVoiceBroadcastButton(props),
props.showPollsButton ? pollButton(room, props.relation) : null,
showLocationButton(props, room, roomId, matrixClient),
];
}
@ -265,7 +269,7 @@ const UploadButton = () => {
/>;
};
function showStickersButton(props: IProps): ReactElement {
function showStickersButton(props: IProps): ReactElement | null {
return (
props.showStickersButton
? <CollapsibleButton
@ -280,7 +284,21 @@ function showStickersButton(props: IProps): ReactElement {
);
}
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
const startVoiceBroadcastButton: React.FC<IProps> = (props: IProps): ReactElement | null => {
return (
props.showVoiceBroadcastButton
? <CollapsibleButton
key="start_voice_broadcast"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceBroadcast"
onClick={props.onStartVoiceBroadcastClick}
title={_t("Voice broadcast")}
/>
: null
);
};
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
// XXX: recording UI does not work well in narrow mode, so hide for now
return (
narrow
@ -312,7 +330,7 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
this.context?.(); // close overflow menu
const canSend = this.props.room.currentState.maySendEvent(
M_POLL_START.name,
MatrixClientPeg.get().getUserId(),
MatrixClientPeg.get().getUserId()!,
);
if (!canSend) {
Modal.createDialog(
@ -362,14 +380,16 @@ function showLocationButton(
room: Room,
roomId: string,
matrixClient: MatrixClient,
): ReactElement {
): ReactElement | null {
const sender = room.getMember(matrixClient.getUserId()!);
return (
props.showLocationButton
props.showLocationButton && sender
? <LocationButton
key="location"
roomId={roomId}
relation={props.relation}
sender={room.getMember(matrixClient.getUserId())}
sender={sender}
menuPosition={props.menuPosition}
/>
: null

View File

@ -34,13 +34,13 @@ function cancelQuoting(context: TimelineRenderingType) {
interface IProps {
permalinkCreator: RoomPermalinkCreator;
replyToEvent: MatrixEvent;
replyToEvent?: MatrixEvent;
}
export default class ReplyPreview extends React.Component<IProps> {
public static contextType = RoomContext;
public render(): JSX.Element {
public render(): JSX.Element | null {
if (!this.props.replyToEvent) return null;
return <div className="mx_ReplyPreview">

View File

@ -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",

View File

@ -101,6 +101,10 @@ export enum LabGroup {
Developer,
}
export enum Features {
VoiceBroadcast = "feature_voice_broadcast",
}
export const labGroupNames: Record<LabGroup, string> = {
[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,

View File

@ -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<React.ComponentProps<typeof MessageComposer>>;
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<React.ComponentProps<typeof MessageComposer>> = {},
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 = {

View File

@ -34,7 +34,7 @@ const mockProps: React.ComponentProps<typeof MessageComposerButtons> = {
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(
<MessageComposerButtons
{...mockProps}
isMenuOpen={false}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
{...mockProps}
/>,
false,
);
@ -63,11 +63,11 @@ describe("MessageComposerButtons", () => {
it("Renders other buttons in menu in wide mode", () => {
const buttons = wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
{...mockProps}
/>,
false,
);
@ -88,11 +88,11 @@ describe("MessageComposerButtons", () => {
it("Renders only some buttons in narrow mode", () => {
const buttons = wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={false}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
{...mockProps}
/>,
true,
);
@ -106,11 +106,11 @@ describe("MessageComposerButtons", () => {
it("Renders other buttons in menu (except voice messages) in narrow mode", () => {
const buttons = wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
{...mockProps}
/>,
true,
);
@ -131,11 +131,11 @@ describe("MessageComposerButtons", () => {
it('should render when asked to', () => {
const buttons = wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
{...mockProps}
/>,
true,
);
@ -155,11 +155,11 @@ describe("MessageComposerButtons", () => {
it('should not render when asked not to', () => {
const buttons = wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={false} // !! the change from the alternate test
showStickersButton={true}
{...mockProps}
/>,
true,
);
@ -176,6 +176,35 @@ describe("MessageComposerButtons", () => {
]);
});
});
describe("with showVoiceBroadcastButton = true", () => {
it("should render the »Voice broadcast« button", () => {
const buttons = wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
showVoiceBroadcastButton={true}
/>,
false,
);
expect(buttonLabels(buttons)).toEqual([
"Emoji",
"Attachment",
"More options",
[
"Sticker",
"Voice Message",
"Voice broadcast",
"Poll",
"Location",
],
]);
});
});
});
function wrapAndRender(component: React.ReactElement, narrow: boolean): ReactWrapper {

View File

@ -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('<SendMessageComposer/>', () => {
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(),

View File

@ -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();
});