Redesign the picture-in-picture window (#9800)
* Remove unnecessary PipContainer component * Redesign the picture-in-picture window * Add a hover effect to the controls * Clarify that WidgetPip has call-specific behaviorpull/28788/head^2
parent
6b155620e4
commit
cb1af0d3de
cypress/e2e/widgets
res
css
components/views/pips
views
messages
rooms
img
element-icons/call
voip
call-view
themes
dark/css
legacy-dark/css
legacy-light/css
light/css
src
components
hooks
test/components/structures
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0084 7.75648C10.3211 7.69163 6.85136 8.12949 6.00781 8.35133C5.95792 8.36445 5.90044 8.37912 5.83616 8.39552C4.54101 8.72607 0.48272 9.76183 0.0442423 13.0436C-0.295466 15.5862 1.40575 16.3558 2.25618 16.2386C2.84479 16.1648 4.5301 15.8983 6.08724 15.6189C7.61629 15.3446 7.61551 14.3359 7.61499 13.6538C7.61498 13.6413 7.61497 13.6288 7.61497 13.6165L7.61497 12.2453C7.61497 11.8961 7.94315 11.6942 8.3958 11.6396C9.99822 11.422 11.3359 11.4213 12.0055 11.4213L12.0112 11.4213C12.6807 11.4213 14.0018 11.422 15.6042 11.6396C16.0569 11.6942 16.385 11.8961 16.385 12.2453L16.385 13.6165C16.385 13.6289 16.385 13.6413 16.385 13.6538C16.3845 14.3359 16.3837 15.3446 17.9128 15.619C19.4699 15.8983 21.1552 16.1648 21.7438 16.2386C22.5942 16.3558 24.2955 15.5862 23.9558 13.0436C23.5173 9.76183 19.459 8.72608 18.1638 8.39553C18.0996 8.37913 18.0421 8.36446 17.9922 8.35134C17.1487 8.1295 13.6956 7.69163 12.0084 7.75648Z" fill="black"/>
|
||||
<path d="M8.02698 15.9613C9.16801 17.1932 11.9148 19.3263 12.6635 19.7641C12.7078 19.79 12.7585 19.8201 12.8152 19.8538C13.9576 20.5329 17.5373 22.6609 20.1454 20.6694C22.1661 19.1266 21.5091 17.3909 20.8289 16.875C20.3633 16.5128 18.9914 15.5145 17.7006 14.6152C16.4331 13.7322 15.7268 14.4397 15.2492 14.918C15.2404 14.9268 15.2317 14.9355 15.2231 14.9442L14.2621 15.9051C14.0174 16.1498 13.6451 16.0605 13.2886 15.7804C12.0092 14.8061 11.0681 13.8659 10.5972 13.395L10.5933 13.391C10.1225 12.9202 9.19387 11.9908 8.21957 10.7114C7.93949 10.3548 7.85018 9.9826 8.09489 9.73789L9.05586 8.77693C9.06448 8.7683 9.0732 8.7596 9.08199 8.75082C9.56034 8.27321 10.2678 7.56684 9.38479 6.29937C8.48555 5.0086 7.4872 3.6367 7.125 3.17106C6.60907 2.49094 4.87345 1.83392 3.33056 3.85458C1.33907 6.46274 3.46708 10.0424 4.1462 11.1848C4.17991 11.2415 4.21005 11.2922 4.23593 11.3365C4.67367 12.0851 6.79507 14.8202 8.02698 15.9613Z" fill="currentColor"/>
|
||||
<path d="M20.6971 3.07817C20.94 2.83153 20.94 2.43163 20.6971 2.18499C20.4542 1.93834 20.0603 1.93834 19.8174 2.18499L17 5.04555L14.1826 2.18499C13.9397 1.93834 13.5458 1.93834 13.3029 2.18499C13.06 2.43163 13.06 2.83153 13.3029 3.07817L16.1203 5.93873L13.1822 8.92183C12.9393 9.16847 12.9393 9.56837 13.1822 9.81501C13.4251 10.0617 13.819 10.0617 14.0619 9.81501L17 6.83192L19.9381 9.81501C20.181 10.0617 20.5749 10.0617 20.8178 9.81501C21.0607 9.56837 21.0607 9.16847 20.8178 8.92182L17.8797 5.93873L20.6971 3.07817Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.0071 7.39308C9.57295 7.33796 6.62364 7.71014 5.90662 7.8987C5.86422 7.90986 5.81537 7.92232 5.76073 7.93627C4.65985 8.21723 1.2103 9.09763 0.837594 11.8872C0.548842 14.0484 1.99488 14.7025 2.71774 14.6029C3.21806 14.5402 4.65057 14.3136 5.97414 14.0762C7.27383 13.843 7.27317 12.9856 7.27273 12.4058C7.27272 12.3951 7.27271 12.3846 7.27271 12.3741L7.27271 11.2085C7.27271 10.9117 7.55166 10.7401 7.93642 10.6937C9.29847 10.5087 10.4355 10.5082 11.0047 10.5082L11.0095 10.5082C11.5786 10.5082 12.7015 10.5087 14.0636 10.6938C14.4483 10.7401 14.7273 10.9117 14.7273 11.2085L14.7273 12.3741C14.7273 12.3846 14.7273 12.3952 14.7273 12.4058C14.7268 12.9856 14.7261 13.843 16.0258 14.0762C17.3494 14.3136 18.7819 14.5402 19.2822 14.6029C20.0051 14.7025 21.4511 14.0484 21.1624 11.8872C20.7897 9.09763 17.3401 8.21724 16.2393 7.93628C16.1846 7.92233 16.1358 7.90986 16.0934 7.89871C15.3763 7.71015 12.4412 7.33796 11.0071 7.39308Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.0 KiB |
|
@ -1,4 +0,0 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.35116 10.6409C6.11185 11.4622 7.94306 12.8843 8.44217 13.1761C8.47169 13.1934 8.50549 13.2135 8.54328 13.2359C9.30489 13.6887 11.6913 15.1074 13.4301 13.7797C14.7772 12.7511 14.3392 11.594 13.8858 11.2501C13.5754 11.0086 12.6608 10.3431 11.8003 9.74356C10.9553 9.15489 10.4844 9.62653 10.1659 9.94543C10.1601 9.95129 10.1543 9.9571 10.1485 9.96285L9.50791 10.6035C9.34477 10.7666 9.0966 10.7071 8.8589 10.5204C8.00599 9.87084 7.37856 9.24399 7.06465 8.93008L7.06201 8.92744C6.74815 8.61357 6.12909 7.99392 5.47955 7.14101C5.29283 6.90331 5.23329 6.65515 5.39643 6.49201L6.03708 5.85136C6.04283 5.84561 6.04864 5.83981 6.0545 5.83396C6.3734 5.51555 6.84504 5.04464 6.25636 4.19966C5.65687 3.33915 4.9913 2.42455 4.74984 2.11412C4.40588 1.66071 3.2488 1.22269 2.22021 2.5698C0.89255 4.30858 2.31122 6.69502 2.76397 7.45663C2.78644 7.49443 2.80653 7.52822 2.82379 7.55774C3.11562 8.05685 4.52989 9.88025 5.35116 10.6409Z" fill="#737D8C"/>
|
||||
<path d="M13.7979 2.05203C13.9599 1.8876 13.9599 1.62101 13.7979 1.45658C13.636 1.29214 13.3734 1.29214 13.2114 1.45658L11.3332 3.36362L9.4549 1.45658C9.29295 1.29214 9.03037 1.29214 8.86842 1.45658C8.70647 1.62101 8.70647 1.8876 8.86842 2.05203L10.7467 3.95907L8.78797 5.9478C8.62602 6.11223 8.62602 6.37883 8.78797 6.54326C8.94992 6.70769 9.21249 6.70769 9.37444 6.54326L11.3332 4.55453L13.2919 6.54326C13.4538 6.70769 13.7164 6.70769 13.8784 6.54326C14.0403 6.37883 14.0403 6.11223 13.8784 5.9478L11.9196 3.95907L13.7979 2.05203Z" fill="#737D8C"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.6 KiB |
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<IProps> {
|
|||
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<IProps> {
|
|||
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<IProps> {
|
|||
className={this.props.className}
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
onClickCapture={this.onClickCapture}
|
||||
onDoubleClick={this.props.onDoubleClick}
|
||||
>
|
||||
{children}
|
|
@ -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<VoiceBroadcastRecording>;
|
||||
voiceBroadcastPreRecording?: Optional<VoiceBroadcastPreRecording>;
|
||||
voiceBroadcastPlayback?: Optional<VoiceBroadcastPlayback>;
|
||||
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
|
||||
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
|
||||
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
|
||||
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<string>): [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<IProps, IState> {
|
||||
// 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<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -146,7 +126,6 @@ class PipView extends React.Component<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
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<ViewRoomPayload>({
|
||||
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<IProps, IState> {
|
|||
}
|
||||
|
||||
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 }) => (
|
||||
<div className={pipViewClasses}>
|
||||
<LegacyCallViewHeader
|
||||
onPipMouseDown={(event) => {
|
||||
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}
|
||||
/>
|
||||
<PersistentApp
|
||||
persistentWidgetId={this.state.persistentWidgetId}
|
||||
persistentRoomId={roomId}
|
||||
pointerEvents={this.state.moving ? "none" : undefined}
|
||||
movePersistedElement={this.movePersistedElement}
|
||||
/>
|
||||
</div>
|
||||
<WidgetPip
|
||||
widgetId={this.state.persistentWidgetId}
|
||||
room={MatrixClientPeg.get().getRoom(this.state.persistentRoomId)!}
|
||||
viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId}
|
||||
onStartMoving={onStartMoving}
|
||||
movePersistedElement={this.props.movePersistedElement}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -448,7 +348,7 @@ class PipView extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
const PipViewHOC: React.FC<IProps> = (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<IProps> = (props) => {
|
|||
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
|
||||
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
|
||||
|
||||
const movePersistedElement = useRef<() => void>();
|
||||
|
||||
return (
|
||||
<PipView
|
||||
<PipContainerInner
|
||||
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
|
||||
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
|
||||
voiceBroadcastRecording={currentVoiceBroadcastRecording}
|
||||
{...props}
|
||||
movePersistedElement={movePersistedElement}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PipViewHOC;
|
|
@ -85,7 +85,7 @@ interface IProps {
|
|||
widgetPageTitle?: string;
|
||||
showLayoutButtons?: boolean;
|
||||
// Handle to manually notify the PersistedElement that it needs to move
|
||||
movePersistedElement?: MutableRefObject<() => void>;
|
||||
movePersistedElement?: MutableRefObject<(() => void) | undefined>;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -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<HTMLDivElement>;
|
||||
|
||||
// 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<IProps> {
|
|||
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 <div ref={this.collectChildContainer} />;
|
||||
|
|
|
@ -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<IProps> {
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, MutableRefObject, useCallback, useMemo } from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { useCallForWidget } from "../../../hooks/useCall";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import { Icon as BackIcon } from "../../../../res/img/element-icons/back.svg";
|
||||
import { Icon as HangupIcon } from "../../../../res/img/element-icons/call/hangup.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface Props {
|
||||
widgetId: string;
|
||||
room: Room;
|
||||
viewingRoom: boolean;
|
||||
onStartMoving: (e: React.MouseEvent<Element, MouseEvent>) => 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<Props> = ({ 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<ViewRoomPayload>({
|
||||
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<ViewRoomPayload>({
|
||||
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 (
|
||||
<div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}>
|
||||
<Toolbar className="mx_WidgetPip_header">
|
||||
<RovingAccessibleButton
|
||||
onClick={onBackClick}
|
||||
className="mx_WidgetPip_backButton"
|
||||
aria-label={_t("Back")}
|
||||
>
|
||||
<BackIcon className="mx_Icon mx_Icon_16" />
|
||||
{roomName}
|
||||
</RovingAccessibleButton>
|
||||
</Toolbar>
|
||||
<PersistentApp
|
||||
persistentWidgetId={widgetId}
|
||||
persistentRoomId={room.roomId}
|
||||
pointerEvents="none"
|
||||
movePersistedElement={movePersistedElement}
|
||||
/>
|
||||
{(call !== null || WidgetType.JITSI.matches(widget.type)) && (
|
||||
<Toolbar className="mx_WidgetPip_footer">
|
||||
<RovingAccessibleTooltipButton
|
||||
onClick={onLeaveClick}
|
||||
tooltip={_t("Leave")}
|
||||
aria-label={_t("Leave")}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
<HangupIcon className="mx_Icon mx_Icon_24" />
|
||||
</RovingAccessibleTooltipButton>
|
||||
</Toolbar>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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<IProps, IState> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="mx_PiPContainer">
|
||||
<PipView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
<PictureInPictureDragger draggable={true}>
|
||||
{[
|
||||
({ onStartMoving }) => (
|
||||
<div onMouseDown={onStartMoving} onClick={clickSpy}>
|
||||
Hello
|
||||
</div>
|
||||
),
|
||||
]}
|
||||
</PictureInPictureDragger>,
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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<MatrixClient>;
|
||||
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(<PipView />);
|
||||
const PipContainer = wrapInMatrixClientContext(wrapInSdkContext(UnwrappedPipContainer, sdkContext));
|
||||
render(<PipContainer />);
|
||||
};
|
||||
|
||||
const viewRoom = (roomId: string) =>
|
||||
const viewRoom = (roomId: string) => {
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||
{
|
||||
action: Action.ViewRoom,
|
||||
|
@ -140,8 +153,9 @@ describe("PipView", () => {
|
|||
},
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
const withCall = async (fn: () => Promise<void>): Promise<void> => {
|
||||
const withCall = async (fn: (call: MockedCall) => Promise<void>): Promise<void> => {
|
||||
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<void>): Promise<void> => {
|
||||
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<ClientWidgetApi["transport"]["send"]>,
|
||||
Parameters<ClientWidgetApi["transport"]["send"]>
|
||||
>()
|
||||
.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" });
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue