/g, "\n") // this is from pressing enter, then typing inside the div
+ .replace(/<\/div>/g, "");
+}
+
export function usePlainTextListeners(
initialContent?: string,
onChange?: (content: string) => void,
@@ -44,25 +55,39 @@ export function usePlainTextListeners(
[onChange],
);
+ const enterShouldSend = !useSettingValue
("MessageComposerInput.ctrlEnterToSend");
const onInput = useCallback(
(event: SyntheticEvent) => {
if (isDivElement(event.target)) {
- setText(event.target.innerHTML);
+ // if enterShouldSend, we do not need to amend the html before setting text
+ const newInnerHTML = enterShouldSend ? event.target.innerHTML : amendInnerHtml(event.target.innerHTML);
+ setText(newInnerHTML);
}
},
- [setText],
+ [setText, enterShouldSend],
);
- const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend");
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
- if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
- event.preventDefault();
- event.stopPropagation();
- send();
+ if (event.key === Key.ENTER) {
+ const sendModifierIsPressed = IS_MAC ? event.metaKey : event.ctrlKey;
+
+ // if enter should send, send if the user is not pushing shift
+ if (enterShouldSend && !event.shiftKey) {
+ event.preventDefault();
+ event.stopPropagation();
+ send();
+ }
+
+ // if enter should not send, send only if the user is pushing ctrl/cmd
+ if (!enterShouldSend && sendModifierIsPressed) {
+ event.preventDefault();
+ event.stopPropagation();
+ send();
+ }
}
},
- [isCtrlEnter, send],
+ [enterShouldSend, send],
);
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };
diff --git a/src/components/views/rooms/wysiwyg_composer/index.ts b/src/components/views/rooms/wysiwyg_composer/index.ts
index c82f59ca89..92cf97032b 100644
--- a/src/components/views/rooms/wysiwyg_composer/index.ts
+++ b/src/components/views/rooms/wysiwyg_composer/index.ts
@@ -17,5 +17,6 @@ limitations under the License.
export {
DynamicImportSendWysiwygComposer as SendWysiwygComposer,
DynamicImportEditWysiwygComposer as EditWysiwygComposer,
+ dynamicImportSendMessage as sendMessage,
+ dynamicImportConversionFunctions as getConversionFunctions,
} from "./DynamicImportWysiwygComposer";
-export { sendMessage } from "./utils/message";
diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts
index a6c2146e67..0819b758d8 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts
@@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg";
import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
-import { htmlSerializeFromMdIfNeeded } from "../../../../../editor/serialize";
import SettingsStore from "../../../../../settings/SettingsStore";
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
import { addReplyToMessageContent } from "../../../../../utils/Reply";
-import { htmlToPlainText } from "../../../../../utils/room/htmlToPlaintext";
// Merges favouring the given relation
function attachRelation(content: IContent, relation?: IEventRelation): void {
@@ -62,7 +61,7 @@ interface CreateMessageContentParams {
editedEvent?: MatrixEvent;
}
-export function createMessageContent(
+export async function createMessageContent(
message: string,
isHTML: boolean,
{
@@ -72,7 +71,7 @@ export function createMessageContent(
includeReplyLegacyFallback = true,
editedEvent,
}: CreateMessageContentParams,
-): IContent {
+): Promise {
// TODO emote ?
const isEditing = Boolean(editedEvent);
@@ -90,26 +89,22 @@ export function createMessageContent(
// const body = textSerialize(model);
- // TODO remove this ugly hack for replace br tag
- const body = (isHTML && htmlToPlainText(message)) || message.replace(/
/g, "\n");
+ // if we're editing rich text, the message content is pure html
+ // BUT if we're not, the message content will be plain text
+ const body = isHTML ? await richToPlain(message) : message;
const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || "";
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
const content: IContent = {
// TODO emote
msgtype: MsgType.Text,
- // TODO when available, use HTML --> Plain text conversion from wysiwyg rust model
body: isEditing ? `${bodyPrefix} * ${body}` : body,
};
// TODO markdown support
const isMarkdownEnabled = SettingsStore.getValue("MessageComposerInput.useMarkdown");
- const formattedBody = isHTML
- ? message
- : isMarkdownEnabled
- ? htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply })
- : null;
+ const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message) : null;
if (formattedBody) {
content.format = "org.matrix.custom.html";
diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index 8039bbe194..18878a97d1 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
-import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
@@ -34,7 +34,7 @@ import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
import { createMessageContent } from "./createMessageContent";
import { isContentModified } from "./isContentModified";
-interface SendMessageParams {
+export interface SendMessageParams {
mxClient: MatrixClient;
relation?: IEventRelation;
replyToEvent?: MatrixEvent;
@@ -43,10 +43,18 @@ interface SendMessageParams {
includeReplyLegacyFallback?: boolean;
}
-export function sendMessage(message: string, isHTML: boolean, { roomContext, mxClient, ...params }: SendMessageParams) {
+export async function sendMessage(
+ message: string,
+ isHTML: boolean,
+ { roomContext, mxClient, ...params }: SendMessageParams,
+) {
const { relation, replyToEvent } = params;
const { room } = roomContext;
- const { roomId } = room;
+ const roomId = room?.roomId;
+
+ if (!roomId) {
+ return;
+ }
const posthogEvent: ComposerEvent = {
eventName: "Composer",
@@ -63,7 +71,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
}*/
PosthogAnalytics.instance.trackEvent(posthogEvent);
- let content: IContent;
+ const content = await createMessageContent(message, isHTML, params);
// TODO slash comment
@@ -71,10 +79,6 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
// TODO quick reaction
- if (!content) {
- content = createMessageContent(message, isHTML, params);
- }
-
// don't bother sending an empty message
if (!content.body.trim()) {
return;
@@ -84,7 +88,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
decorateStartSendingTime(content);
}
- const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
+ const threadId = relation?.event_id && relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
const prom = doMaybeLocalRoomAction(
roomId,
@@ -139,7 +143,7 @@ interface EditMessageParams {
editorStateTransfer: EditorStateTransfer;
}
-export function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
+export async function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
const editedEvent = editorStateTransfer.getEvent();
PosthogAnalytics.instance.trackEvent({
@@ -156,7 +160,7 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
}*/
- const editContent = createMessageContent(html, true, { editedEvent });
+ const editContent = await createMessageContent(html, true, { editedEvent });
const newContent = editContent["m.new_content"];
const shouldSend = true;
@@ -174,10 +178,10 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr
let response: Promise | undefined;
- // If content is modified then send an updated event into the room
- if (isContentModified(newContent, editorStateTransfer)) {
- const roomId = editedEvent.getRoomId();
+ const roomId = editedEvent.getRoomId();
+ // If content is modified then send an updated event into the room
+ if (isContentModified(newContent, editorStateTransfer) && roomId) {
// TODO Slash Commands
if (shouldSend) {
diff --git a/src/components/views/voip/PipContainer.tsx b/src/components/views/voip/PipContainer.tsx
deleted file mode 100644
index b206181855..0000000000
--- a/src/components/views/voip/PipContainer.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
-Copyright 2021 Šimon Brandner
-
-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.
-*/
-
-import React from "react";
-
-import PipView from "./PipView";
-
-interface IProps {}
-
-interface IState {}
-
-export default class PiPContainer extends React.PureComponent {
- public render() {
- return (
-
- );
- }
-}
diff --git a/src/events/forward/getForwardableEvent.ts b/src/events/forward/getForwardableEvent.ts
index ac6132de11..7d1782d7ae 100644
--- a/src/events/forward/getForwardableEvent.ts
+++ b/src/events/forward/getForwardableEvent.ts
@@ -19,6 +19,7 @@ import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation";
+import { VoiceBroadcastInfoEventType } from "../../voice-broadcast/types";
/**
* Get forwardable event for a given event
@@ -29,6 +30,8 @@ export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): Matr
return null;
}
+ if (event.getType() === VoiceBroadcastInfoEventType) return null;
+
// Live location beacons should forward their latest location as a static pin location
// If the beacon is not live, or doesn't have a location forwarding is not allowed
if (M_BEACON_INFO.matches(event.getType())) {
diff --git a/src/hooks/useCall.ts b/src/hooks/useCall.ts
index b90533f0ac..03bee56b9c 100644
--- a/src/hooks/useCall.ts
+++ b/src/hooks/useCall.ts
@@ -33,6 +33,11 @@ export const useCall = (roomId: string): Call | null => {
return call;
};
+export const useCallForWidget = (widgetId: string, roomId: string): Call | null => {
+ const call = useCall(roomId);
+ return call?.widget.id === widgetId ? call : null;
+};
+
export const useConnectionState = (call: Call): ConnectionState =>
useTypedEventEmitterState(
call,
diff --git a/src/models/Call.ts b/src/models/Call.ts
index 2b996c96c3..eaee7df2e8 100644
--- a/src/models/Call.ts
+++ b/src/models/Call.ts
@@ -255,6 +255,7 @@ export abstract class Call extends TypedEventEmitter {
+ if (uid === this.widgetUid) {
+ logger.log("The widget died; treating this as a user hangup");
+ this.setDisconnected();
+ }
+ };
+
private beforeUnload = () => this.setDisconnected();
}
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index eeebabd5c1..2459a9f745 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -32,6 +32,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext";
import { launchPollEditor } from "../components/views/messages/MPollBody";
import { Action } from "../dispatcher/actions";
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
+import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast/types";
/**
* Returns whether an event should allow actions like reply, reactions, edit, etc.
@@ -56,7 +57,9 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
} else if (
mxEvent.getType() === "m.sticker" ||
M_POLL_START.matches(mxEvent.getType()) ||
- M_BEACON_INFO.matches(mxEvent.getType())
+ M_BEACON_INFO.matches(mxEvent.getType()) ||
+ (mxEvent.getType() === VoiceBroadcastInfoEventType &&
+ mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started)
) {
return true;
}
diff --git a/src/utils/room/htmlToPlaintext.ts b/src/utils/room/htmlToPlaintext.ts
deleted file mode 100644
index 4b0272b4e1..0000000000
--- a/src/utils/room/htmlToPlaintext.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-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.
-*/
-
-export function htmlToPlainText(html: string) {
- return new DOMParser().parseFromString(html, "text/html").documentElement.textContent;
-}
diff --git a/test/components/views/voip/PictureInPictureDragger-test.tsx b/test/components/structures/PictureInPictureDragger-test.tsx
similarity index 68%
rename from test/components/views/voip/PictureInPictureDragger-test.tsx
rename to test/components/structures/PictureInPictureDragger-test.tsx
index 9df7cb07ad..9b92fefd79 100644
--- a/test/components/views/voip/PictureInPictureDragger-test.tsx
+++ b/test/components/structures/PictureInPictureDragger-test.tsx
@@ -15,11 +15,10 @@ limitations under the License.
*/
import React from "react";
-import { render, RenderResult } from "@testing-library/react";
+import { screen, render, RenderResult } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
-import PictureInPictureDragger, {
- CreatePipChildren,
-} from "../../../../src/components/views/voip/PictureInPictureDragger";
+import PictureInPictureDragger, { CreatePipChildren } from "../../../src/components/structures/PictureInPictureDragger";
describe("PictureInPictureDragger", () => {
let renderResult: RenderResult;
@@ -82,4 +81,29 @@ describe("PictureInPictureDragger", () => {
expect(renderResult.container).toMatchSnapshot();
});
});
+
+ it("doesn't leak drag events to children as clicks", async () => {
+ const clickSpy = jest.fn();
+ render(
+
+ {[
+ ({ onStartMoving }) => (
+
+ Hello
+
+ ),
+ ]}
+ ,
+ );
+ const target = screen.getByText("Hello");
+
+ // A click without a drag motion should go through
+ await userEvent.pointer([{ keys: "[MouseLeft>]", target }, { keys: "[/MouseLeft]" }]);
+ expect(clickSpy).toHaveBeenCalled();
+
+ // A drag motion should not trigger a click
+ clickSpy.mockClear();
+ await userEvent.pointer([{ keys: "[MouseLeft>]", target }, { coords: { x: 60, y: 60 } }, "[/MouseLeft]"]);
+ expect(clickSpy).not.toHaveBeenCalled();
+ });
});
diff --git a/test/components/views/voip/PipView-test.tsx b/test/components/structures/PipContainer-test.tsx
similarity index 67%
rename from test/components/views/voip/PipView-test.tsx
rename to test/components/structures/PipContainer-test.tsx
index 6da5f1d6f8..5ca118c451 100644
--- a/test/components/views/voip/PipView-test.tsx
+++ b/test/components/structures/PipContainer-test.tsx
@@ -16,12 +16,14 @@ limitations under the License.
import React from "react";
import { mocked, Mocked } from "jest-mock";
-import { screen, render, act, cleanup, fireEvent, waitFor } from "@testing-library/react";
+import { screen, render, act, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { Widget, ClientWidgetApi } from "matrix-widget-api";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
+import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {
@@ -34,18 +36,19 @@ import {
wrapInMatrixClientContext,
wrapInSdkContext,
mkRoomCreateEvent,
+ mockPlatformPeg,
flushPromises,
-} from "../../../test-utils";
-import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
-import { CallStore } from "../../../../src/stores/CallStore";
-import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
-import UnwrappedPipView from "../../../../src/components/views/voip/PipView";
-import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
-import DMRoomMap from "../../../../src/utils/DMRoomMap";
-import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
-import { Action } from "../../../../src/dispatcher/actions";
-import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
-import { TestSdkContext } from "../../../TestSdkContext";
+} from "../../test-utils";
+import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
+import { CallStore } from "../../../src/stores/CallStore";
+import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
+import { PipContainer as UnwrappedPipContainer } from "../../../src/components/structures/PipContainer";
+import ActiveWidgetStore from "../../../src/stores/ActiveWidgetStore";
+import DMRoomMap from "../../../src/utils/DMRoomMap";
+import defaultDispatcher from "../../../src/dispatcher/dispatcher";
+import { Action } from "../../../src/dispatcher/actions";
+import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
+import { TestSdkContext } from "../../TestSdkContext";
import {
VoiceBroadcastInfoState,
VoiceBroadcastPlaybacksStore,
@@ -53,15 +56,21 @@ import {
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
-} from "../../../../src/voice-broadcast";
-import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
-import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
-import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators";
+} from "../../../src/voice-broadcast";
+import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils";
+import { RoomViewStore } from "../../../src/stores/RoomViewStore";
+import { IRoomStateEventsActionPayload } from "../../../src/actions/MatrixActionCreators";
+import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
+import WidgetStore from "../../../src/stores/WidgetStore";
+import { WidgetType } from "../../../src/widgets/WidgetType";
+import { SdkContextClass } from "../../../src/contexts/SDKContext";
+import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
-describe("PipView", () => {
+describe("PipContainer", () => {
useMockedCalls();
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
+ let user: UserEvent;
let sdkContext: TestSdkContext;
let client: Mocked;
let room: Room;
@@ -78,6 +87,8 @@ describe("PipView", () => {
};
beforeEach(async () => {
+ user = userEvent.setup();
+
stubClient();
client = mocked(MatrixClientPeg.get());
DMRoomMap.makeShared();
@@ -110,6 +121,8 @@ describe("PipView", () => {
);
sdkContext = new TestSdkContext();
+ // @ts-ignore PipContainer uses SDKContext in the constructor
+ SdkContextClass.instance = sdkContext;
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore);
@@ -127,11 +140,11 @@ describe("PipView", () => {
});
const renderPip = () => {
- const PipView = wrapInMatrixClientContext(wrapInSdkContext(UnwrappedPipView, sdkContext));
- render();
+ const PipContainer = wrapInMatrixClientContext(wrapInSdkContext(UnwrappedPipContainer, sdkContext));
+ render();
};
- const viewRoom = (roomId: string) =>
+ const viewRoom = (roomId: string) => {
defaultDispatcher.dispatch(
{
action: Action.ViewRoom,
@@ -140,8 +153,9 @@ describe("PipView", () => {
},
true,
);
+ };
- const withCall = async (fn: () => Promise): Promise => {
+ const withCall = async (fn: (call: MockedCall) => Promise): Promise => {
MockedCall.create(room, "1");
const call = CallStore.instance.getCall(room.roomId);
if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
@@ -156,16 +170,16 @@ describe("PipView", () => {
ActiveWidgetStore.instance.setWidgetPersistence(widget.id, room.roomId, true);
});
- await fn();
+ await fn(call);
cleanup();
call.destroy();
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
};
- const withWidget = (fn: () => void): void => {
+ const withWidget = async (fn: () => Promise): Promise => {
act(() => ActiveWidgetStore.instance.setWidgetPersistence("1", room.roomId, true));
- fn();
+ await fn();
cleanup();
ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId);
};
@@ -197,7 +211,7 @@ describe("PipView", () => {
};
const setUpRoomViewStore = () => {
- new RoomViewStore(defaultDispatcher, sdkContext);
+ sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext);
};
const mkVoiceBroadcast = (room: Room): MatrixEvent => {
@@ -220,54 +234,104 @@ describe("PipView", () => {
expect(screen.queryByRole("complementary")).toBeNull();
});
- it("shows an active call with a maximise button", async () => {
+ it("shows an active call with back and leave buttons", async () => {
renderPip();
- await withCall(async () => {
+ await withCall(async (call) => {
screen.getByRole("complementary");
- screen.getByText(room.roomId);
- expect(screen.queryByRole("button", { name: "Pin" })).toBeNull();
- expect(screen.queryByRole("button", { name: /return/i })).toBeNull();
- // The maximise button should jump to the call
+ // The return button should jump to the call
const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
- fireEvent.click(screen.getByRole("button", { name: "Fill screen" }));
- await waitFor(() =>
- expect(dispatcherSpy).toHaveBeenCalledWith({
- action: Action.ViewRoom,
- room_id: room.roomId,
- view_call: true,
- }),
- );
+ await user.click(screen.getByRole("button", { name: "Back" }));
+ expect(dispatcherSpy).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ room_id: room.roomId,
+ view_call: true,
+ metricsTrigger: expect.any(String),
+ });
defaultDispatcher.unregister(dispatcherRef);
+
+ // The leave button should disconnect from the call
+ const disconnectSpy = jest.spyOn(call, "disconnect");
+ await user.click(screen.getByRole("button", { name: "Leave" }));
+ expect(disconnectSpy).toHaveBeenCalled();
});
});
- it("shows a persistent widget with pin and maximise buttons when viewing the room", () => {
+ it("shows a persistent widget with back button when viewing the room", async () => {
+ setUpRoomViewStore();
viewRoom(room.roomId);
+ const widget = WidgetStore.instance.addVirtualWidget(
+ {
+ id: "1",
+ creatorUserId: "@alice:exaxmple.org",
+ type: WidgetType.CUSTOM.preferred,
+ url: "https://example.org",
+ name: "Example widget",
+ },
+ room.roomId,
+ );
renderPip();
- withWidget(() => {
+ await withWidget(async () => {
screen.getByRole("complementary");
- screen.getByText(room.roomId);
- screen.getByRole("button", { name: "Pin" });
- screen.getByRole("button", { name: "Fill screen" });
- expect(screen.queryByRole("button", { name: /return/i })).toBeNull();
+
+ // The return button should maximize the widget
+ const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
+ await user.click(screen.getByRole("button", { name: "Back" }));
+ expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center);
+
+ expect(screen.queryByRole("button", { name: "Leave" })).toBeNull();
});
+
+ WidgetStore.instance.removeVirtualWidget("1", room.roomId);
});
- it("shows a persistent widget with a return button when not viewing the room", () => {
+ it("shows a persistent Jitsi widget with back and leave buttons when not viewing the room", async () => {
+ mockPlatformPeg({ supportsJitsiScreensharing: () => true });
+ setUpRoomViewStore();
viewRoom(room2.roomId);
+ const widget = WidgetStore.instance.addVirtualWidget(
+ {
+ id: "1",
+ creatorUserId: "@alice:exaxmple.org",
+ type: WidgetType.JITSI.preferred,
+ url: "https://meet.example.org",
+ name: "Jitsi example",
+ },
+ room.roomId,
+ );
renderPip();
- withWidget(() => {
+ await withWidget(async () => {
screen.getByRole("complementary");
- screen.getByText(room.roomId);
- expect(screen.queryByRole("button", { name: "Pin" })).toBeNull();
- expect(screen.queryByRole("button", { name: "Fill screen" })).toBeNull();
- screen.getByRole("button", { name: /return/i });
+
+ // The return button should view the room
+ const dispatcherSpy = jest.fn();
+ const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
+ await user.click(screen.getByRole("button", { name: "Back" }));
+ expect(dispatcherSpy).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ room_id: room.roomId,
+ metricsTrigger: expect.any(String),
+ });
+ defaultDispatcher.unregister(dispatcherRef);
+
+ // The leave button should hangup the call
+ const sendSpy = jest
+ .fn<
+ ReturnType,
+ Parameters
+ >()
+ .mockResolvedValue({});
+ const mockMessaging = { transport: { send: sendSpy }, stop: () => {} } as unknown as ClientWidgetApi;
+ WidgetMessagingStore.instance.storeMessaging(new Widget(widget), room.roomId, mockMessaging);
+ await user.click(screen.getByRole("button", { name: "Leave" }));
+ expect(sendSpy).toHaveBeenCalledWith(ElementWidgetActions.HangupCall, {});
});
+
+ WidgetStore.instance.removeVirtualWidget("1", room.roomId);
});
describe("when there is a voice broadcast recording and pre-recording", () => {
@@ -287,8 +351,8 @@ describe("PipView", () => {
await withCall(async () => {
// Broadcast: Check for the „Live“ badge to be present
expect(screen.queryByText("Live")).toBeInTheDocument();
- // Call: Check for the „Fill screen“ button to be present
- expect(screen.queryByLabelText("Fill screen")).toBeInTheDocument();
+ // Call: Check for the „Leave“ button to be present
+ screen.getByRole("button", { name: "Leave" });
});
});
});
diff --git a/test/components/views/voip/__snapshots__/PictureInPictureDragger-test.tsx.snap b/test/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap
similarity index 100%
rename from test/components/views/voip/__snapshots__/PictureInPictureDragger-test.tsx.snap
rename to test/components/structures/__snapshots__/PictureInPictureDragger-test.tsx.snap
diff --git a/test/components/views/beacon/BeaconMarker-test.tsx b/test/components/views/beacon/BeaconMarker-test.tsx
index 14b2032395..90b578e89b 100644
--- a/test/components/views/beacon/BeaconMarker-test.tsx
+++ b/test/components/views/beacon/BeaconMarker-test.tsx
@@ -15,8 +15,7 @@ limitations under the License.
*/
import React from "react";
-// eslint-disable-next-line deprecate/import
-import { mount } from "enzyme";
+import { render, screen } from "@testing-library/react";
import * as maplibregl from "maplibre-gl";
import { act } from "react-dom/test-utils";
import { Beacon, Room, RoomMember, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
@@ -43,6 +42,7 @@ describe("", () => {
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
const mockMap = new maplibregl.Map(mapOptions);
+ const mockMarker = new maplibregl.Marker();
const mockClient = getMockClientWithEventEmitter({
getClientWellKnown: jest.fn().mockReturnValue({
@@ -64,14 +64,16 @@ describe("", () => {
const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
const notLiveEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-2");
+ const geoUri1 = "geo:51,41";
const location1 = makeBeaconEvent(aliceId, {
beaconInfoId: defaultEvent.getId(),
- geoUri: "geo:51,41",
+ geoUri: geoUri1,
timestamp: now + 1,
});
+ const geoUri2 = "geo:52,42";
const location2 = makeBeaconEvent(aliceId, {
beaconInfoId: defaultEvent.getId(),
- geoUri: "geo:52,42",
+ geoUri: geoUri2,
timestamp: now + 10000,
});
@@ -80,11 +82,15 @@ describe("", () => {
beacon: new Beacon(defaultEvent),
};
- const getComponent = (props = {}) =>
- mount(, {
- wrappingComponent: MatrixClientContext.Provider,
- wrappingComponentProps: { value: mockClient },
+ const renderComponent = (props = {}) => {
+ const Wrapper = (wrapperProps = {}) => {
+ return ;
+ };
+
+ return render(, {
+ wrapper: Wrapper,
});
+ };
beforeEach(() => {
jest.clearAllMocks();
@@ -93,38 +99,45 @@ describe("", () => {
it("renders nothing when beacon is not live", () => {
const room = setupRoom([notLiveEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(notLiveEvent));
- const component = getComponent({ beacon });
- expect(component.html()).toBe(null);
+ const { asFragment } = renderComponent({ beacon });
+ expect(asFragment()).toMatchInlineSnapshot(``);
+ expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument();
});
it("renders nothing when beacon has no location", () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
- const component = getComponent({ beacon });
- expect(component.html()).toBe(null);
+ const { asFragment } = renderComponent({ beacon });
+ expect(asFragment()).toMatchInlineSnapshot(``);
+ expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument();
});
it("renders marker when beacon has location", () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
- beacon.addLocations([location1]);
- const component = getComponent({ beacon });
- expect(component).toMatchSnapshot();
+ beacon?.addLocations([location1]);
+ const { asFragment } = renderComponent({ beacon });
+ expect(asFragment()).toMatchSnapshot();
+ expect(screen.getByTestId("avatar-img")).toBeInTheDocument();
});
it("updates with new locations", () => {
+ const lonLat1 = { lon: 41, lat: 51 };
+ const lonLat2 = { lon: 42, lat: 52 };
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
- beacon.addLocations([location1]);
- const component = getComponent({ beacon });
- expect(component.find("SmartMarker").props()["geoUri"]).toEqual("geo:51,41");
+ beacon?.addLocations([location1]);
+ // render the component then add a new location, check mockMarker called as expected
+ renderComponent({ beacon });
+ expect(mockMarker.setLngLat).toHaveBeenLastCalledWith(lonLat1);
+ expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
+
+ // add a location, check mockMarker called with new location details
act(() => {
- beacon.addLocations([location2]);
+ beacon?.addLocations([location2]);
});
- component.setProps({});
-
- // updated to latest location
- expect(component.find("SmartMarker").props()["geoUri"]).toEqual("geo:52,42");
+ expect(mockMarker.setLngLat).toHaveBeenLastCalledWith(lonLat2);
+ expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
});
});
diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
index 3871be4a83..b42ccb83ee 100644
--- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
+++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
@@ -1,240 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` renders marker when beacon has location 1`] = `
-
-
-
-
+
+
+
-
-
-
-
-
-
-
- A
-
-
-
-
-
-
-
-
-
-
-
-
+
+ A
+
+

+
+
+
+
+
`;
diff --git a/test/components/views/context_menus/MessageContextMenu-test.tsx b/test/components/views/context_menus/MessageContextMenu-test.tsx
index 46a17ec786..3fdf26832d 100644
--- a/test/components/views/context_menus/MessageContextMenu-test.tsx
+++ b/test/components/views/context_menus/MessageContextMenu-test.tsx
@@ -42,14 +42,15 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { ReadPinsEventId } from "../../../../src/components/views/right_panel/types";
import { Action } from "../../../../src/dispatcher/actions";
+import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
+import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
jest.mock("../../../../src/utils/strings", () => ({
copyPlaintext: jest.fn(),
getSelectedText: jest.fn(),
}));
jest.mock("../../../../src/utils/EventUtils", () => ({
- // @ts-ignore don't mock everything
- ...jest.requireActual("../../../../src/utils/EventUtils"),
+ ...(jest.requireActual("../../../../src/utils/EventUtils") as object),
canEditContent: jest.fn(),
}));
jest.mock("../../../../src/dispatcher/dispatcher");
@@ -241,6 +242,17 @@ describe("MessageContextMenu", () => {
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(0);
});
+ it("should not allow forwarding a voice broadcast", () => {
+ const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
+ roomId,
+ VoiceBroadcastInfoState.Started,
+ "@user:example.com",
+ "ABC123",
+ );
+ const menu = createMenu(broadcastStartEvent);
+ expect(menu.find('div[aria-label="Forward"]')).toHaveLength(0);
+ });
+
describe("forwarding beacons", () => {
const aliceId = "@alice:server.org";
diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx
index a3a3ffe141..8b64f205f0 100644
--- a/test/components/views/messages/MessageActionBar-test.tsx
+++ b/test/components/views/messages/MessageActionBar-test.tsx
@@ -15,8 +15,7 @@ limitations under the License.
*/
import React from "react";
-import { render, fireEvent } from "@testing-library/react";
-import { act } from "react-test-renderer";
+import { act, render, fireEvent } from "@testing-library/react";
import { EventType, EventStatus, MatrixEvent, MatrixEventEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
@@ -34,6 +33,8 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { Action } from "../../../../src/dispatcher/actions";
import { UserTab } from "../../../../src/components/views/dialogs/UserTab";
+import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
+import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
jest.mock("../../../../src/dispatcher/dispatcher");
@@ -405,6 +406,17 @@ describe("", () => {
expect(queryByLabelText("Reply in thread")).toBeTruthy();
});
+ it("does not render thread button for a voice broadcast", () => {
+ const broadcastEvent = mkVoiceBroadcastInfoStateEvent(
+ roomId,
+ VoiceBroadcastInfoState.Started,
+ userId,
+ "ABC123",
+ );
+ const { queryByLabelText } = getComponent({ mxEvent: broadcastEvent });
+ expect(queryByLabelText("Reply in thread")).not.toBeInTheDocument();
+ });
+
it("opens user settings on click", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx
index 7f64be2437..d9343208c4 100644
--- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx
@@ -229,7 +229,10 @@ describe("EditWysiwygComposer", () => {
},
"msgtype": "m.text",
};
- expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent);
+ await waitFor(() =>
+ expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent),
+ );
+
expect(spyDispatcher).toBeCalledWith({ action: "message_sent" });
});
});
diff --git a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx
index ed421f50af..6d16a3b152 100644
--- a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx
@@ -19,6 +19,8 @@ import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PlainTextComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
+import * as mockUseSettingsHook from "../../../../../../src/hooks/useSettings";
+import * as mockKeyboard from "../../../../../../src/Keyboard";
describe("PlainTextComposer", () => {
const customRender = (
@@ -37,6 +39,17 @@ describe("PlainTextComposer", () => {
);
};
+ let mockUseSettingValue: jest.SpyInstance;
+ beforeEach(() => {
+ // defaults for these tests are:
+ // ctrlEnterToSend is false
+ mockUseSettingValue = jest.spyOn(mockUseSettingsHook, "useSettingValue").mockReturnValue(false);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
it("Should have contentEditable at false when disabled", () => {
// When
customRender(jest.fn(), jest.fn(), true);
@@ -64,7 +77,7 @@ describe("PlainTextComposer", () => {
expect(onChange).toBeCalledWith(content);
});
- it("Should call onSend when Enter is pressed", async () => {
+ it("Should call onSend when Enter is pressed when ctrlEnterToSend is false", async () => {
//When
const onSend = jest.fn();
customRender(jest.fn(), onSend);
@@ -74,9 +87,134 @@ describe("PlainTextComposer", () => {
expect(onSend).toBeCalledTimes(1);
});
+ it("Should not call onSend when Enter is pressed when ctrlEnterToSend is true", async () => {
+ //When
+ mockUseSettingValue.mockReturnValue(true);
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ await userEvent.type(screen.getByRole("textbox"), "{enter}");
+
+ // Then it does not send a message
+ expect(onSend).toBeCalledTimes(0);
+ });
+
+ it("Should only call onSend when ctrl+enter is pressed when ctrlEnterToSend is true on windows", async () => {
+ //When
+ mockUseSettingValue.mockReturnValue(true);
+
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ const textBox = screen.getByRole("textbox");
+ await userEvent.type(textBox, "hello");
+
+ // Then it does NOT send a message on enter
+ await userEvent.type(textBox, "{enter}");
+ expect(onSend).toBeCalledTimes(0);
+
+ // Then it does NOT send a message on windows+enter
+ await userEvent.type(textBox, "{meta>}{enter}{meta/}");
+ expect(onSend).toBeCalledTimes(0);
+
+ // Then it does send a message on ctrl+enter
+ await userEvent.type(textBox, "{control>}{enter}{control/}");
+ expect(onSend).toBeCalledTimes(1);
+ });
+
+ it("Should only call onSend when cmd+enter is pressed when ctrlEnterToSend is true on mac", async () => {
+ //When
+ mockUseSettingValue.mockReturnValue(true);
+ Object.defineProperty(mockKeyboard, "IS_MAC", { value: true });
+
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ const textBox = screen.getByRole("textbox");
+ await userEvent.type(textBox, "hello");
+
+ // Then it does NOT send a message on enter
+ await userEvent.type(textBox, "{enter}");
+ expect(onSend).toBeCalledTimes(0);
+
+ // Then it does NOT send a message on ctrl+enter
+ await userEvent.type(textBox, "{control>}{enter}{control/}");
+ expect(onSend).toBeCalledTimes(0);
+
+ // Then it does send a message on cmd+enter
+ await userEvent.type(textBox, "{meta>}{enter}{meta/}");
+ expect(onSend).toBeCalledTimes(1);
+ });
+
+ it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is false", async () => {
+ //When
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ const textBox = screen.getByRole("textbox");
+ const inputWithShiftEnter = "new{Shift>}{enter}{/Shift}line";
+ const expectedInnerHtml = "new\nline";
+
+ await userEvent.click(textBox);
+ await userEvent.type(textBox, inputWithShiftEnter);
+
+ // Then it does not send a message, but inserts a newline character
+ expect(onSend).toBeCalledTimes(0);
+ expect(textBox.innerHTML).toBe(expectedInnerHtml);
+ });
+
+ it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is true", async () => {
+ //When
+ mockUseSettingValue.mockReturnValue(true);
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ const textBox = screen.getByRole("textbox");
+ const keyboardInput = "new{Shift>}{enter}{/Shift}line";
+ const expectedInnerHtml = "new\nline";
+
+ await userEvent.click(textBox);
+ await userEvent.type(textBox, keyboardInput);
+
+ // Then it does not send a message, but inserts a newline character
+ expect(onSend).toBeCalledTimes(0);
+ expect(textBox.innerHTML).toBe(expectedInnerHtml);
+ });
+
+ it("Should not insert div and br tags when enter is pressed when ctrlEnterToSend is true", async () => {
+ //When
+ mockUseSettingValue.mockReturnValue(true);
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ const textBox = screen.getByRole("textbox");
+ const enterThenTypeHtml = "hello
{
+ //When
+ mockUseSettingValue.mockReturnValue(true);
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ const textBox = screen.getByRole("textbox");
+ const defaultEnterHtml = "
{
//When
- let composer;
+ let composer: {
+ clear: () => void;
+ insertText: (text: string) => void;
+ };
+
render(
{(ref, composerFunctions) => {
@@ -85,9 +223,11 @@ describe("PlainTextComposer", () => {
}}
,
);
+
await userEvent.type(screen.getByRole("textbox"), "content");
expect(screen.getByRole("textbox").innerHTML).toBe("content");
- composer.clear();
+
+ composer!.clear();
// Then
expect(screen.getByRole("textbox").innerHTML).toBeFalsy();
@@ -112,7 +252,7 @@ describe("PlainTextComposer", () => {
render();
// Then
- expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("false");
+ expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("false");
expect(editor).toBe(screen.getByRole("textbox"));
// When
@@ -126,7 +266,7 @@ describe("PlainTextComposer", () => {
});
// Then
- expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("true");
+ expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("true");
jest.useRealTimers();
(global.ResizeObserver as jest.Mock).mockRestore();
diff --git a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts
index e654186617..340a4c1af2 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts
@@ -24,7 +24,7 @@ describe("createMessageContent", () => {
return "$$permalink$$";
},
} as RoomPermalinkCreator;
- const message = "hello world";
+ const message = "hello world";
const mockEvent = mkEvent({
type: "m.room.message",
room: "myfakeroom",
@@ -37,31 +37,31 @@ describe("createMessageContent", () => {
jest.resetAllMocks();
});
- it("Should create html message", () => {
+ it("Should create html message", async () => {
// When
- const content = createMessageContent(message, true, { permalinkCreator });
+ const content = await createMessageContent(message, true, { permalinkCreator });
// Then
expect(content).toEqual({
- body: "hello world",
+ body: "*__hello__ world*",
format: "org.matrix.custom.html",
formatted_body: message,
msgtype: "m.text",
});
});
- it("Should add reply to message content", () => {
+ it("Should add reply to message content", async () => {
// When
- const content = createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
+ const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
// Then
expect(content).toEqual({
- "body": "> Replying to this\n\nhello world",
+ "body": "> Replying to this\n\n*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body":
'In reply to' +
' myfakeuser' +
- "
Replying to this
hello world",
+ "
Replying to thishello world",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
@@ -71,17 +71,17 @@ describe("createMessageContent", () => {
});
});
- it("Should add relation to message", () => {
+ it("Should add relation to message", async () => {
// When
const relation = {
rel_type: "m.thread",
event_id: "myFakeThreadId",
};
- const content = createMessageContent(message, true, { permalinkCreator, relation });
+ const content = await createMessageContent(message, true, { permalinkCreator, relation });
// Then
expect(content).toEqual({
- "body": "hello world",
+ "body": "*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body": message,
"msgtype": "m.text",
@@ -92,7 +92,7 @@ describe("createMessageContent", () => {
});
});
- it("Should add fields related to edition", () => {
+ it("Should add fields related to edition", async () => {
// When
const editedEvent = mkEvent({
type: "m.room.message",
@@ -110,16 +110,16 @@ describe("createMessageContent", () => {
},
event: true,
});
- const content = createMessageContent(message, true, { permalinkCreator, editedEvent });
+ const content = await createMessageContent(message, true, { permalinkCreator, editedEvent });
// Then
expect(content).toEqual({
- "body": " * hello world",
+ "body": " * *__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body": ` * ${message}`,
"msgtype": "m.text",
"m.new_content": {
- body: "hello world",
+ body: "*__hello__ world*",
format: "org.matrix.custom.html",
formatted_body: message,
msgtype: "m.text",
diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
index ceb00ade79..733d5c117e 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -70,6 +70,79 @@ describe("message", () => {
expect(spyDispatcher).toBeCalledTimes(0);
});
+ it("Should not send message when there is no roomId", async () => {
+ // When
+ const mockRoomWithoutId = mkStubRoom("", "room without id", mockClient) as any;
+ const mockRoomContextWithoutId: IRoomState = getRoomContext(mockRoomWithoutId, {});
+
+ await sendMessage(message, true, {
+ roomContext: mockRoomContextWithoutId,
+ mxClient: mockClient,
+ permalinkCreator,
+ });
+
+ // Then
+ expect(mockClient.sendMessage).toBeCalledTimes(0);
+ expect(spyDispatcher).toBeCalledTimes(0);
+ });
+
+ describe("calls client.sendMessage with", () => {
+ it("a null argument if SendMessageParams is missing relation", async () => {
+ // When
+ await sendMessage(message, true, {
+ roomContext: defaultRoomContext,
+ mxClient: mockClient,
+ permalinkCreator,
+ });
+
+ // Then
+ expect(mockClient.sendMessage).toHaveBeenCalledWith(expect.anything(), null, expect.anything());
+ });
+ it("a null argument if SendMessageParams has relation but relation is missing event_id", async () => {
+ // When
+ await sendMessage(message, true, {
+ roomContext: defaultRoomContext,
+ mxClient: mockClient,
+ permalinkCreator,
+ relation: {},
+ });
+
+ // Then
+ expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything());
+ });
+ it("a null argument if SendMessageParams has relation but rel_type does not match THREAD_RELATION_TYPE.name", async () => {
+ // When
+ await sendMessage(message, true, {
+ roomContext: defaultRoomContext,
+ mxClient: mockClient,
+ permalinkCreator,
+ relation: {
+ event_id: "valid_id",
+ rel_type: "m.does_not_match",
+ },
+ });
+
+ // Then
+ expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything());
+ });
+
+ it("the event_id if SendMessageParams has relation and rel_type matches THREAD_RELATION_TYPE.name", async () => {
+ // When
+ await sendMessage(message, true, {
+ roomContext: defaultRoomContext,
+ mxClient: mockClient,
+ permalinkCreator,
+ relation: {
+ event_id: "valid_id",
+ rel_type: "m.thread",
+ },
+ });
+
+ // Then
+ expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), "valid_id", expect.anything());
+ });
+ });
+
it("Should send html message", async () => {
// When
await sendMessage(message, true, {
@@ -80,7 +153,7 @@ describe("message", () => {
// Then
const expectedContent = {
- body: "hello world",
+ body: "*__hello__ world*",
format: "org.matrix.custom.html",
formatted_body: "hello world",
msgtype: "m.text",
@@ -114,7 +187,7 @@ describe("message", () => {
});
const expectedContent = {
- "body": "> My reply\n\nhello world",
+ "body": "> My reply\n\n*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body":
'In reply to' +
diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts
index ad6bb362dd..17f4fbfc4a 100644
--- a/test/models/Call-test.ts
+++ b/test/models/Call-test.ts
@@ -784,6 +784,13 @@ describe("ElementCall", () => {
expect(call.connectionState).toBe(ConnectionState.Connected);
});
+ it("disconnects if the widget dies", async () => {
+ await call.connect();
+ expect(call.connectionState).toBe(ConnectionState.Connected);
+ WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
+ expect(call.connectionState).toBe(ConnectionState.Disconnected);
+ });
+
it("tracks participants in room state", async () => {
expect(call.participants).toEqual(new Map());
diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts
index ca77e64662..decf42931a 100644
--- a/test/utils/EventUtils-test.ts
+++ b/test/utils/EventUtils-test.ts
@@ -43,6 +43,8 @@ import {
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../test-utils";
import dis from "../../src/dispatcher/dispatcher";
import { Action } from "../../src/dispatcher/actions";
+import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
+import { VoiceBroadcastInfoState } from "../../src/voice-broadcast/types";
jest.mock("../../src/dispatcher/dispatcher");
@@ -151,6 +153,20 @@ describe("EventUtils", () => {
},
});
+ const voiceBroadcastStart = mkVoiceBroadcastInfoStateEvent(
+ "!room:example.com",
+ VoiceBroadcastInfoState.Started,
+ "@user:example.com",
+ "ABC123",
+ );
+
+ const voiceBroadcastStop = mkVoiceBroadcastInfoStateEvent(
+ "!room:example.com",
+ VoiceBroadcastInfoState.Stopped,
+ "@user:example.com",
+ "ABC123",
+ );
+
describe("isContentActionable()", () => {
type TestCase = [string, MatrixEvent];
it.each([
@@ -161,6 +177,7 @@ describe("EventUtils", () => {
["room member event", roomMemberEvent],
["event without msgtype", noMsgType],
["event without content body property", noContentBody],
+ ["broadcast stop event", voiceBroadcastStop],
])("returns false for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(false);
});
@@ -171,6 +188,7 @@ describe("EventUtils", () => {
["event with empty content body", emptyContentBody],
["event with a content body", niceTextMessage],
["beacon_info event", beaconInfoEvent],
+ ["broadcast start event", voiceBroadcastStart],
])("returns true for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(true);
});
diff --git a/test/voice-broadcast/utils/test-utils.ts b/test/voice-broadcast/utils/test-utils.ts
index cbf0a5989a..fc1ffd4b15 100644
--- a/test/voice-broadcast/utils/test-utils.ts
+++ b/test/voice-broadcast/utils/test-utils.ts
@@ -21,7 +21,7 @@ import {
VoiceBroadcastChunkEventType,
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
-} from "../../../src/voice-broadcast";
+} from "../../../src/voice-broadcast/types";
import { mkEvent } from "../../test-utils";
// timestamp incremented on each call to prevent duplicate timestamp
diff --git a/yarn.lock b/yarn.lock
index 0a0353d772..e7e33c7db3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1525,10 +1525,10 @@
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6"
integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA==
-"@matrix-org/matrix-wysiwyg@^0.11.0":
- version "0.11.0"
- resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.11.0.tgz#3000ee809a3e38242c5da47bef17c572582f2f6b"
- integrity sha512-B16iLfNnW4PKG4fpDuwJVc0QUrUUqTkhwJ/kxzawcxwVNmWbsPCWJ3hkextYrN2gqRL1d4CNASkNbWLCNNiXhA==
+"@matrix-org/matrix-wysiwyg@^0.13.0":
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.13.0.tgz#e643df4e13cdc5dbf9285740bc0ce2aef9873c16"
+ integrity sha512-MCeTj4hkl0snjlygd1v+mEEOgaN6agyjAVjJEbvEvP/BaYaDiPEXMTDaRQrcUt3OIY53UNhm1DDEn4yPTn83Jg==
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
version "3.2.14"