diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts index 2518ffebda..47232dc5af 100644 --- a/cypress/e2e/spaces/spaces.spec.ts +++ b/cypress/e2e/spaces/spaces.spec.ts @@ -77,8 +77,10 @@ describe("Spaces", () => { cy.stopSynapse(synapse); }); - it("should allow user to create public space", () => { - openSpaceCreateMenu().within(() => { + it.only("should allow user to create public space", () => { + openSpaceCreateMenu(); + cy.get("#mx_ContextualMenu_Container").percySnapshotElement("Space create menu"); + cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu").within(() => { cy.get(".mx_SpaceCreateMenuType_public").click(); cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile( "cypress/fixtures/riot.png", diff --git a/cypress/e2e/widgets/widget-pip-close.spec.ts b/cypress/e2e/widgets/widget-pip-close.spec.ts index a88fac9ca5..59376d8572 100644 --- a/cypress/e2e/widgets/widget-pip-close.spec.ts +++ b/cypress/e2e/widgets/widget-pip-close.spec.ts @@ -145,7 +145,7 @@ describe("Widget PIP", () => { win.mxActiveWidgetStore.setWidgetPersistence(DEMO_WIDGET_ID, roomId, true); // checks that pip window is opened - cy.get(".mx_LegacyCallView_pip").should("exist"); + cy.get(".mx_WidgetPip").should("exist"); // checks that widget is opened in pip cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => { @@ -164,7 +164,7 @@ describe("Widget PIP", () => { } // checks that pip window is closed - cy.get(".mx_LegacyCallView_pip").should("not.exist"); + cy.get(".mx_WidgetPip").should("not.exist"); }); }); }); diff --git a/package.json b/package.json index 4d5a725671..37f8a2d895 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.3.0", - "@matrix-org/matrix-wysiwyg": "^0.11.0", + "@matrix-org/matrix-wysiwyg": "^0.13.0", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/res/css/_components.pcss b/res/css/_components.pcss index cb025f5b8c..9cd446ecbc 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -30,6 +30,7 @@ @import "./components/views/location/_ZoomButtons.pcss"; @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; +@import "./components/views/pips/_WidgetPip.pcss"; @import "./components/views/settings/devices/_CurrentDeviceSection.pcss"; @import "./components/views/settings/devices/_DeviceDetailHeading.pcss"; @import "./components/views/settings/devices/_DeviceDetails.pcss"; @@ -373,7 +374,6 @@ @import "./views/voip/_LegacyCallViewForRoom.pcss"; @import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss"; -@import "./views/voip/_PiPContainer.pcss"; @import "./views/voip/_VideoFeed.pcss"; @import "./voice-broadcast/atoms/_LiveBadge.pcss"; @import "./voice-broadcast/atoms/_PlaybackControlButton.pcss"; diff --git a/res/css/components/views/pips/_WidgetPip.pcss b/res/css/components/views/pips/_WidgetPip.pcss new file mode 100644 index 0000000000..80c4771925 --- /dev/null +++ b/res/css/components/views/pips/_WidgetPip.pcss @@ -0,0 +1,68 @@ +/* +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. +*/ + +.mx_WidgetPip { + width: 320px; + height: 220px; + border-radius: 8px; + contain: paint; + color: $call-primary-content; + cursor: pointer; +} + +.mx_WidgetPip_header, +.mx_WidgetPip_footer { + position: absolute; + left: 0; + height: 60px; + width: 100%; + box-sizing: border-box; + transition: opacity ease 0.15s; + + .mx_WidgetPip:not(:hover) > & { + opacity: 0; + } +} + +.mx_WidgetPip_header { + top: 0; + padding: $spacing-12; + display: flex; + font-size: $font-12px; + font-weight: $font-semi-bold; + background: linear-gradient(rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0)); +} + +.mx_WidgetPip_backButton { + height: $spacing-24; + display: flex; + align-items: center; + gap: $spacing-12; + + > .mx_Icon { + color: $call-light-quaternary-content; + padding: 0; + } +} + +.mx_WidgetPip_footer { + bottom: 0; + padding: $spacing-12 $spacing-8; + display: flex; + justify-content: flex-end; + align-items: flex-end; + background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.9)); +} diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index f5d567d93f..633c94dfa3 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -38,9 +38,9 @@ $SpaceRoomViewInnerWidth: 428px; &::before { position: absolute; content: ""; - width: 32px; - height: 32px; - top: 24px; + width: 24px; + height: 24px; + top: 27px; left: 20px; mask-position: center; mask-repeat: no-repeat; diff --git a/res/css/views/dialogs/_JoinRuleDropdown.pcss b/res/css/views/dialogs/_JoinRuleDropdown.pcss index ddb16c31df..83d73fcea2 100644 --- a/res/css/views/dialogs/_JoinRuleDropdown.pcss +++ b/res/css/views/dialogs/_JoinRuleDropdown.pcss @@ -50,8 +50,10 @@ limitations under the License. } .mx_JoinRuleDropdown_invite::before { + box-sizing: border-box; mask-image: url("$(res)/img/element-icons/lock.svg"); mask-size: contain; + padding: 1px; } .mx_JoinRuleDropdown_public::before { diff --git a/res/css/views/messages/_LegacyCallEvent.pcss b/res/css/views/messages/_LegacyCallEvent.pcss index c51b486e66..873f5d4b5b 100644 --- a/res/css/views/messages/_LegacyCallEvent.pcss +++ b/res/css/views/messages/_LegacyCallEvent.pcss @@ -68,7 +68,7 @@ limitations under the License. &.mx_LegacyCallEvent_rejected, &.mx_LegacyCallEvent_noAnswer { .mx_LegacyCallEvent_type_icon::before { - mask-image: url("$(res)/img/voip/declined-voice.svg"); + mask-image: url("$(res)/img/element-icons/call/hangup.svg"); } } } diff --git a/res/css/views/rooms/_AppsDrawer.pcss b/res/css/views/rooms/_AppsDrawer.pcss index 16e19149fd..91b84ef445 100644 --- a/res/css/views/rooms/_AppsDrawer.pcss +++ b/res/css/views/rooms/_AppsDrawer.pcss @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$MiniAppTileHeight: 200px; +$MiniAppTileHeight: 220px; /* TODO this should be 300px but that's too large */ $MinWidth: 240px; diff --git a/res/css/views/spaces/_SpaceCreateMenu.pcss b/res/css/views/spaces/_SpaceCreateMenu.pcss index f1697dacb0..9bb95de2b6 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.pcss +++ b/res/css/views/spaces/_SpaceCreateMenu.pcss @@ -58,8 +58,10 @@ $spacePanelWidth: 68px; .mx_SpaceCreateMenuType_public::before { mask-image: url("$(res)/img/globe.svg"); } + .mx_SpaceCreateMenuType_private::before { mask-image: url("$(res)/img/element-icons/lock.svg"); + mask-size: 18px; } .mx_SpaceCreateMenu_back { diff --git a/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss b/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss index ebc1652999..8e24318ef9 100644 --- a/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss +++ b/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss @@ -163,7 +163,7 @@ limitations under the License. background-color: $alert; &::before { - mask-image: url("$(res)/img/voip/call-view/hangup.svg"); + mask-image: url("$(res)/img/element-icons/call/hangup.svg"); background-color: white; /* Same on both themes */ } } diff --git a/res/css/views/voip/_CallView.pcss b/res/css/views/voip/_CallView.pcss index b320d8588e..3e214a5b7b 100644 --- a/res/css/views/voip/_CallView.pcss +++ b/res/css/views/voip/_CallView.pcss @@ -32,7 +32,7 @@ limitations under the License. height: 100%; border: none; border-radius: inherit; - background-color: $call-lobby-background; + background-color: $call-background; } /* While the lobby is shown, the widget needs to stay loaded but hidden in the background */ @@ -44,10 +44,10 @@ limitations under the License. min-height: 0; flex-grow: 1; padding: $spacing-12; - color: $call-lobby-primary-content; - background-color: $call-lobby-background; + color: $call-primary-content; + background-color: $call-background; - --facepile-background: $call-lobby-background; + --facepile-background: $call-background; border-radius: 8px; display: flex; @@ -66,7 +66,7 @@ limitations under the License. width: 100%; max-width: 800px; aspect-ratio: 1.5; - background-color: $call-lobby-system; + background-color: $call-system; border-radius: 20px; overflow: hidden; @@ -104,7 +104,7 @@ limitations under the License. left: 0; right: 0; - background-color: rgba($call-lobby-background, 0.9); + background-color: rgba($call-background, 0.9); display: flex; justify-content: center; @@ -120,7 +120,7 @@ limitations under the License. width: $size; height: $size; - background-color: $call-lobby-system; + background-color: $call-system; border-radius: calc($size / 2); &::before { @@ -129,7 +129,7 @@ limitations under the License. mask-repeat: no-repeat; mask-size: 20px; mask-position: center; - background-color: $call-lobby-primary-content; + background-color: $call-primary-content; height: 100%; width: 100%; } @@ -153,7 +153,7 @@ limitations under the License. width: $size; height: $size; - background-color: $call-lobby-system; + background-color: $call-system; border-radius: calc($size / 2); &::before { @@ -162,7 +162,7 @@ limitations under the License. mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-size: $size; mask-position: center; - background-color: $call-lobby-primary-content; + background-color: $call-primary-content; height: 100%; width: 100%; } @@ -171,10 +171,10 @@ limitations under the License. &.mx_CallView_deviceButtonWrapper_muted { .mx_CallView_deviceButton, .mx_CallView_deviceListButton { - background-color: $call-lobby-primary-content; + background-color: $call-primary-content; &::before { - background-color: $call-lobby-system; + background-color: $call-system; } } diff --git a/res/css/views/voip/_LegacyCallPreview.pcss b/res/css/views/voip/_LegacyCallPreview.pcss index 01a8874604..a739747195 100644 --- a/res/css/views/voip/_LegacyCallPreview.pcss +++ b/res/css/views/voip/_LegacyCallPreview.pcss @@ -22,8 +22,8 @@ limitations under the License. left: 0; position: fixed; top: 0; - - pointer-events: initial; /* restore pointer events so the user can leave/interact */ + /* Display above any widget elements */ + z-index: 102; .mx_VideoFeed_remote.mx_VideoFeed_voice { min-height: 150px; diff --git a/res/css/views/voip/_PiPContainer.pcss b/res/css/views/voip/_PiPContainer.pcss deleted file mode 100644 index 8038e32bb8..0000000000 --- a/res/css/views/voip/_PiPContainer.pcss +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2020 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. -*/ - -.mx_PiPContainer { - position: absolute; - right: 20px; - bottom: 72px; - z-index: 100; - - /* Disable pointer events for Jitsi widgets to function. Direct */ - /* calls have their own cursor and behaviour, but we need to make */ - /* sure the cursor hits the iframe for Jitsi which will be at a */ - /* different level. */ - pointer-events: none; -} diff --git a/res/img/element-icons/call/hangup.svg b/res/img/element-icons/call/hangup.svg index 1a1b82a1d7..173677db2d 100644 --- a/res/img/element-icons/call/hangup.svg +++ b/res/img/element-icons/call/hangup.svg @@ -1,3 +1,4 @@ - + + diff --git a/res/img/voip/call-view/hangup.svg b/res/img/voip/call-view/hangup.svg deleted file mode 100644 index 255433abdc..0000000000 --- a/res/img/voip/call-view/hangup.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/voip/declined-voice.svg b/res/img/voip/declined-voice.svg deleted file mode 100644 index 78e8d90cdf..0000000000 --- a/res/img/voip/declined-voice.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index 9293d51d45..6fd88d63a9 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -188,9 +188,10 @@ $call-view-content-background: $quinary-content; $video-feed-secondary-background: $system; -$call-lobby-system: $system; -$call-lobby-background: $background; -$call-lobby-primary-content: $primary-content; +$call-system: $system; +$call-background: $background; +$call-primary-content: $primary-content; +$call-light-quaternary-content: #c1c6cd; /* ******************** */ /* Location sharing */ diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss index 2680b366cc..ed1ff5793b 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.pcss +++ b/res/themes/legacy-dark/css/_legacy-dark.pcss @@ -119,9 +119,10 @@ $call-view-content-background: $quinary-content; $video-feed-secondary-background: $system; -$call-lobby-system: $system; -$call-lobby-background: $background; -$call-lobby-primary-content: $primary-content; +$call-system: $system; +$call-background: $background; +$call-primary-content: $primary-content; +$call-light-quaternary-content: #c1c6cd; $roomlist-filter-active-bg-color: $panel-actions; $roomlist-bg-color: $header-panel-bg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index d5d7b4efc2..41a4118576 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -182,9 +182,10 @@ $call-view-content-background: #21262c; $video-feed-secondary-background: #394049; /* XXX: Color from dark theme */ /* All of these are from dark theme */ -$call-lobby-system: #21262c; -$call-lobby-background: #15191e; -$call-lobby-primary-content: #ffffff; +$call-system: #21262c; +$call-background: #15191e; +$call-primary-content: #ffffff; +$call-light-quaternary-content: #c1c6cd; $username-variant1-color: #368bd6; $username-variant2-color: #ac3ba8; diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index a88bacaac9..0b016e9c8d 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -273,9 +273,11 @@ $video-feed-secondary-background: #394049; /* XXX: Color from dark theme */ $voipcall-plinth-color: $system; /* All of these are from dark theme */ -$call-lobby-system: #21262c; -$call-lobby-background: #15191e; -$call-lobby-primary-content: #ffffff; +$call-system: #21262c; +$call-background: #15191e; +$call-primary-content: #ffffff; +/* This one is from light theme */ +$call-light-quaternary-content: #c1c6cd; /* ******************** */ /* One-off colors */ diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index f0e48f44be..6e18f8a6f7 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -41,7 +41,6 @@ import { DefaultTagID } from "../../stores/room-list/models"; import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast"; import { Action } from "../../dispatcher/actions"; import LeftPanel from "./LeftPanel"; -import PipContainer from "../views/voip/PipContainer"; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import RoomListStore from "../../stores/room-list/RoomListStore"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; @@ -71,6 +70,7 @@ import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload import { IConfigOptions } from "../../IConfigOptions"; import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning"; import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage"; +import { PipContainer } from "./PipContainer"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. diff --git a/src/components/views/voip/PictureInPictureDragger.tsx b/src/components/structures/PictureInPictureDragger.tsx similarity index 83% rename from src/components/views/voip/PictureInPictureDragger.tsx rename to src/components/structures/PictureInPictureDragger.tsx index 3ba2c4ab53..1daea9eb89 100644 --- a/src/components/views/voip/PictureInPictureDragger.tsx +++ b/src/components/structures/PictureInPictureDragger.tsx @@ -16,9 +16,9 @@ limitations under the License. import React, { createRef } from "react"; -import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; -import { lerp } from "../../../utils/AnimationUtils"; -import { MarkedExecution } from "../../../utils/MarkedExecution"; +import UIStore, { UI_EVENTS } from "../../stores/UIStore"; +import { lerp } from "../../utils/AnimationUtils"; +import { MarkedExecution } from "../../utils/MarkedExecution"; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; @@ -65,12 +65,20 @@ export default class PictureInPictureDragger extends React.Component { private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT; private translationX = this.desiredTranslationX; private translationY = this.desiredTranslationY; - private moving = false; - private scheduledUpdate = new MarkedExecution( + private mouseHeld = false; + private scheduledUpdate: MarkedExecution = new MarkedExecution( () => this.animationCallback(), () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), ); + private _moving = false; + public get moving(): boolean { + return this._moving; + } + private set moving(value: boolean) { + this._moving = value; + } + public componentDidMount() { document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); @@ -183,26 +191,47 @@ export default class PictureInPictureDragger extends React.Component { event.preventDefault(); event.stopPropagation(); - this.moving = true; - this.initX = event.pageX - this.desiredTranslationX; - this.initY = event.pageY - this.desiredTranslationY; - this.scheduledUpdate.mark(); + this.mouseHeld = true; }; - private onMoving = (event: React.MouseEvent | MouseEvent) => { - if (!this.moving) return; + private onMoving = (event: MouseEvent) => { + if (!this.mouseHeld) return; event.preventDefault(); event.stopPropagation(); + if (!this.moving) { + this.moving = true; + this.initX = event.pageX - this.desiredTranslationX; + this.initY = event.pageY - this.desiredTranslationY; + this.scheduledUpdate.mark(); + } + this.setTranslation(event.pageX - this.initX, event.pageY - this.initY); }; - private onEndMoving = () => { - this.moving = false; + private onEndMoving = (event: MouseEvent) => { + if (!this.mouseHeld) return; + + event.preventDefault(); + event.stopPropagation(); + + this.mouseHeld = false; + // Delaying this to the next event loop tick is necessary for click + // event cancellation to work + setImmediate(() => (this.moving = false)); this.snap(true); }; + private onClickCapture = (event: React.MouseEvent) => { + // To prevent mouse up events during dragging from being double-counted + // as clicks, we cancel clicks before they ever reach the target + if (this.moving) { + event.preventDefault(); + event.stopPropagation(); + } + }; + public render() { const style = { transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`, @@ -220,6 +249,7 @@ export default class PictureInPictureDragger extends React.Component { className={this.props.className} style={style} ref={this.callViewWrapper} + onClickCapture={this.onClickCapture} onDoubleClick={this.props.onDoubleClick} > {children} diff --git a/src/components/views/voip/PipView.tsx b/src/components/structures/PipContainer.tsx similarity index 70% rename from src/components/views/voip/PipView.tsx rename to src/components/structures/PipContainer.tsx index b8940f3209..a932c43e7d 100644 --- a/src/components/views/voip/PipView.tsx +++ b/src/components/structures/PipContainer.tsx @@ -14,28 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, useContext } from "react"; +import React, { MutableRefObject, useContext, useRef } from "react"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; -import classNames from "classnames"; -import { Room } from "matrix-js-sdk/src/models/room"; import { Optional } from "matrix-events-sdk"; -import LegacyCallView from "./LegacyCallView"; -import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler"; -import PersistentApp from "../elements/PersistentApp"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import LegacyCallView from "../views/voip/LegacyCallView"; +import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import PictureInPictureDragger, { CreatePipChildren } from "./PictureInPictureDragger"; -import dis from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; -import LegacyCallViewHeader from "./LegacyCallView/LegacyCallViewHeader"; -import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../stores/ActiveWidgetStore"; -import WidgetStore, { IApp } from "../../../stores/WidgetStore"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; -import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext"; -import { CallStore } from "../../../stores/CallStore"; +import dis from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; +import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; +import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; +import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { useCurrentVoiceBroadcastPreRecording, useCurrentVoiceBroadcastRecording, @@ -46,8 +40,9 @@ import { VoiceBroadcastRecording, VoiceBroadcastRecordingPip, VoiceBroadcastSmallPlaybackBody, -} from "../../../voice-broadcast"; -import { useCurrentVoiceBroadcastPlayback } from "../../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback"; +} from "../../voice-broadcast"; +import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback"; +import { WidgetPip } from "../views/pips/WidgetPip"; const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -59,9 +54,10 @@ const SHOW_CALL_IN_STATES = [ ]; interface IProps { - voiceBroadcastRecording?: Optional; - voiceBroadcastPreRecording?: Optional; - voiceBroadcastPlayback?: Optional; + voiceBroadcastRecording: Optional; + voiceBroadcastPreRecording: Optional; + voiceBroadcastPlayback: Optional; + movePersistedElement: MutableRefObject<(() => void) | undefined>; } interface IState { @@ -78,20 +74,8 @@ interface IState { persistentWidgetId: string; persistentRoomId: string; showWidgetInPip: boolean; - - moving: boolean; } -const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room | null, IApp | null] => { - if (!widgetId) return [null, null]; - if (!roomId) return [null, null]; - - const room = MatrixClientPeg.get().getRoom(roomId); - const app = WidgetStore.instance.getApps(roomId).find((app) => app.id === widgetId); - - return [room, app || null]; -}; - // Splits a list of calls into one 'primary' one and a list // (which should be a single element) of other calls. // The primary will be the one not on hold, or an arbitrary one @@ -128,16 +112,12 @@ function getPrimarySecondaryCallsForPip(roomId: Optional): [MatrixCall | } /** - * PipView shows a small version of the LegacyCallView or a sticky widget hovering over the UI in 'picture-in-picture' - * (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing + * PipContainer shows a small version of the LegacyCallView or a sticky widget hovering over the UI in + * 'picture-in-picture' (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing * and all widgets that are active but not shown in any other possible container. */ -class PipView extends React.Component { - // The cast is not so great, but solves the typing issue for the moment. - // Proper solution: use useRef (requires the component to be refactored to a functional component). - private movePersistedElement = createRef<() => void>() as React.MutableRefObject<() => void>; - +class PipContainerInner extends React.Component { public constructor(props: IProps) { super(props); @@ -146,7 +126,6 @@ class PipView extends React.Component { const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId); this.state = { - moving: false, viewedRoomId: roomId || undefined, primaryCall: primaryCall || null, secondaryCall: secondaryCalls[0], @@ -168,7 +147,6 @@ class PipView extends React.Component { ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence); ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges); ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges); - document.addEventListener("mouseup", this.onEndMoving.bind(this)); } public componentWillUnmount() { @@ -184,18 +162,9 @@ class PipView extends React.Component { ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence); ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges); ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges); - document.removeEventListener("mouseup", this.onEndMoving.bind(this)); } - private onStartMoving() { - this.setState({ moving: true }); - } - - private onEndMoving() { - this.setState({ moving: false }); - } - - private onMove = () => this.movePersistedElement.current?.(); + private onMove = () => this.props.movePersistedElement.current?.(); private onRoomViewStoreUpdate = () => { const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); @@ -265,53 +234,6 @@ class PipView extends React.Component { } }; - private onMaximize = (): void => { - const widgetId = this.state.persistentWidgetId; - const roomId = this.state.persistentRoomId; - - if (this.state.showWidgetInPip && widgetId && roomId) { - const [room, app] = getRoomAndAppForWidget(widgetId, roomId); - - if (room && app) { - WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center); - return; - } - } - - dis.dispatch({ - action: "video_fullscreen", - fullscreen: true, - }); - }; - - private onPin = (): void => { - if (!this.state.showWidgetInPip) return; - - const [room, app] = getRoomAndAppForWidget(this.state.persistentWidgetId, this.state.persistentRoomId); - - if (room && app) { - WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); - } - }; - - private onExpand = (): void => { - const widgetId = this.state.persistentWidgetId; - if (!widgetId || !this.state.showWidgetInPip) return; - - dis.dispatch({ - action: Action.ViewRoom, - room_id: this.state.persistentRoomId, - }); - }; - - private onViewCall = (): void => - dis.dispatch({ - action: Action.ViewRoom, - room_id: this.state.persistentRoomId, - view_call: true, - metricsTrigger: undefined, - }); - // Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId public updateShowWidgetInPip( persistentWidgetId = this.state.persistentWidgetId, @@ -398,36 +320,14 @@ class PipView extends React.Component { } if (this.state.showWidgetInPip) { - const pipViewClasses = classNames({ - mx_LegacyCallView: true, - mx_LegacyCallView_pip: pipMode, - mx_LegacyCallView_large: !pipMode, - }); - const roomId = this.state.persistentRoomId; - const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!; - const viewingCallRoom = this.state.viewedRoomId === roomId; - const isCall = CallStore.instance.getActiveCall(roomId) !== null; - pipContent.push(({ onStartMoving }) => ( -
- { - onStartMoving?.(event); - this.onStartMoving.bind(this)(); - }} - pipMode={pipMode} - callRooms={[roomForWidget]} - onExpand={!isCall && !viewingCallRoom ? this.onExpand : undefined} - onPin={!isCall && viewingCallRoom ? this.onPin : undefined} - onMaximize={isCall ? this.onViewCall : viewingCallRoom ? this.onMaximize : undefined} - /> - -
+ )); } @@ -448,7 +348,7 @@ class PipView extends React.Component { } } -const PipViewHOC: React.FC = (props) => { +export const PipContainer: React.FC = () => { const sdkContext = useContext(SDKContext); const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore; const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore); @@ -459,14 +359,14 @@ const PipViewHOC: React.FC = (props) => { const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore; const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore); + const movePersistedElement = useRef<() => void>(); + return ( - ); }; - -export default PipViewHOC; diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx index 3d712e5828..2a63673b1a 100644 --- a/src/components/views/beacon/BeaconMarker.tsx +++ b/src/components/views/beacon/BeaconMarker.tsx @@ -45,10 +45,12 @@ const BeaconMarker: React.FC = ({ map, beacon, tooltip }) => { return null; } - const geoUri = latestLocationState?.uri; + const geoUri = latestLocationState.uri || ""; - const markerRoomMember = - beacon.beaconInfo.assetType === LocationAssetType.Self ? room.getMember(beacon.beaconInfoOwner) : undefined; + const assetTypeIsSelf = beacon.beaconInfo?.assetType === LocationAssetType.Self; + const _member = room?.getMember(beacon.beaconInfoOwner); + + const markerRoomMember = assetTypeIsSelf && _member ? _member : undefined; return ( void>; + movePersistedElement?: MutableRefObject<(() => void) | undefined>; } interface IState { diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index cfe5f24f96..2bb4037df1 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -16,7 +16,6 @@ limitations under the License. import React, { MutableRefObject } from "react"; import ReactDOM from "react-dom"; -import { throttle } from "lodash"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import dis from "../../../dispatcher/dispatcher"; @@ -58,7 +57,7 @@ interface IProps { style?: React.StyleHTMLAttributes; // Handle to manually notify this PersistedElement that it needs to move - moveRef?: MutableRefObject<() => void>; + moveRef?: MutableRefObject<(() => void) | undefined>; } /** @@ -177,24 +176,20 @@ export default class PersistedElement extends React.Component { child.style.display = visible ? "block" : "none"; } - private updateChildPosition = throttle( - (child: HTMLDivElement, parent: HTMLDivElement): void => { - if (!child || !parent) return; + private updateChildPosition(child: HTMLDivElement, parent: HTMLDivElement): void { + if (!child || !parent) return; - const parentRect = parent.getBoundingClientRect(); - Object.assign(child.style, { - zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex, - position: "absolute", - top: "0", - left: "0", - transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`, - width: parentRect.width + "px", - height: parentRect.height + "px", - }); - }, - 16, - { trailing: true, leading: true }, - ); + const parentRect = parent.getBoundingClientRect(); + Object.assign(child.style, { + zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex, + position: "absolute", + top: "0", + left: "0", + transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`, + width: parentRect.width + "px", + height: parentRect.height + "px", + }); + } public render(): JSX.Element { return
; diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index fc24b2b8c6..f692f74aa8 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -27,7 +27,7 @@ interface IProps { persistentWidgetId: string; persistentRoomId: string; pointerEvents?: string; - movePersistedElement: MutableRefObject<() => void>; + movePersistedElement: MutableRefObject<(() => void) | undefined>; } export default class PersistentApp extends React.Component { diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 3cac66a79c..1ec7fae751 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -59,6 +59,7 @@ import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import useFavouriteMessages from "../../../hooks/useFavouriteMessages"; import { GetRelationsForEvent } from "../rooms/EventTile"; +import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types"; interface IOptionsButtonProps { mxEvent: MatrixEvent; @@ -394,7 +395,8 @@ export default class MessageActionBar extends React.PureComponent) => void; + movePersistedElement: MutableRefObject<(() => void) | undefined>; +} + +/** + * A picture-in-picture view for a widget. Additional controls are shown if the + * widget is a call of some sort. + */ +export const WidgetPip: FC = ({ widgetId, room, viewingRoom, onStartMoving, movePersistedElement }) => { + const widget = useMemo( + () => WidgetStore.instance.getApps(room.roomId).find((app) => app.id === widgetId)!, + [room, widgetId], + ); + + const roomName = useTypedEventEmitterState( + room, + RoomEvent.Name, + useCallback(() => room.name, [room]), + ); + + const call = useCallForWidget(widgetId, room.roomId); + + const onBackClick = useCallback( + (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (call !== null) { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + view_call: true, + metricsTrigger: "WebFloatingCallWindow", + }); + } else if (viewingRoom) { + WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Center); + } else { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + metricsTrigger: "WebFloatingCallWindow", + }); + } + }, + [room, call, widget, viewingRoom], + ); + + const onLeaveClick = useCallback( + (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (call !== null) { + call.disconnect().catch((e) => console.error("Failed to leave call", e)); + } else { + // Assumed to be a Jitsi widget + WidgetMessagingStore.instance + .getMessagingForUid(WidgetUtils.getWidgetUid(widget)) + ?.transport.send(ElementWidgetActions.HangupCall, {}) + .catch((e) => console.error("Failed to leave Jitsi", e)); + } + }, + [call, widget], + ); + + return ( +
+ + + + {roomName} + + + + {(call !== null || WidgetType.JITSI.matches(widget.type)) && ( + + + + + + )} +
+ ); +}; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index b50b285f8f..d7e9a5f71a 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -54,9 +54,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import { Features } from "../../../settings/Settings"; import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; -import { SendWysiwygComposer, sendMessage } from "./wysiwyg_composer/"; +import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/"; import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -import { htmlToPlainText } from "../../../utils/room/htmlToPlaintext"; import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; import { SdkContextClass } from "../../../contexts/SDKContext"; @@ -333,7 +332,7 @@ export class MessageComposer extends React.Component { if (this.state.isWysiwygLabEnabled) { const { permalinkCreator, relation, replyToEvent } = this.props; - sendMessage(this.state.composerContent, this.state.isRichTextEnabled, { + await sendMessage(this.state.composerContent, this.state.isRichTextEnabled, { mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, @@ -358,14 +357,19 @@ export class MessageComposer extends React.Component { }); }; - private onRichTextToggle = () => { - this.setState((state) => ({ - isRichTextEnabled: !state.isRichTextEnabled, - initialComposerContent: !state.isRichTextEnabled - ? state.composerContent - : // TODO when available use rust model plain text - htmlToPlainText(state.composerContent), - })); + private onRichTextToggle = async () => { + const { richToPlain, plainToRich } = await getConversionFunctions(); + + const { isRichTextEnabled, composerContent } = this.state; + const convertedContent = isRichTextEnabled + ? await richToPlain(composerContent) + : await plainToRich(composerContent); + + this.setState({ + isRichTextEnabled: !isRichTextEnabled, + composerContent: convertedContent, + initialComposerContent: convertedContent, + }); }; private onVoiceStoreUpdate = () => { diff --git a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx index 65a365b06d..2799f494de 100644 --- a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx @@ -16,9 +16,25 @@ limitations under the License. import React, { ComponentProps, lazy, Suspense } from "react"; +// we need to import the types for TS, but do not import the sendMessage +// function to avoid importing from "@matrix-org/matrix-wysiwyg" +import { SendMessageParams } from "./utils/message"; + const SendComposer = lazy(() => import("./SendWysiwygComposer")); const EditComposer = lazy(() => import("./EditWysiwygComposer")); +export const dynamicImportSendMessage = async (message: string, isHTML: boolean, params: SendMessageParams) => { + const { sendMessage } = await import("./utils/message"); + + return sendMessage(message, isHTML, params); +}; + +export const dynamicImportConversionFunctions = async () => { + const { richToPlain, plainToRich } = await import("@matrix-org/matrix-wysiwyg"); + + return { richToPlain, plainToRich }; +}; + export function DynamicImportSendWysiwygComposer(props: ComponentProps) { return ( }> diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index fdab7647b9..958396c419 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -17,11 +17,22 @@ limitations under the License. import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react"; import { useSettingValue } from "../../../../../hooks/useSettings"; +import { IS_MAC, Key } from "../../../../../Keyboard"; function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; } +// Hitting enter inside the editor inserts an editable div, initially containing a
+// For correct display, first replace this pattern with a newline character and then remove divs +// noting that they are used to delimit paragraphs +function amendInnerHtml(text: string) { + return text + .replace(/

<\/div>/g, "\n") // this is pressing enter then not typing + .replace(/
/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`] = ` - - - - + +
+
- - - - - + + + +
+
+
+
`; 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"