Merge pull request #6475 from matrix-org/jitsi-picture-in-picture
Refactor Calls components into smaller pieces for future Widgets work
This commit is contained in:
commit
b6f7c4fe95
7 changed files with 530 additions and 407 deletions
|
@ -275,6 +275,7 @@
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
@import "./views/voip/_CallViewSidebar.scss";
|
@import "./views/voip/_CallViewSidebar.scss";
|
||||||
|
@import "./views/voip/_CallViewHeader.scss";
|
||||||
@import "./views/voip/_DialPad.scss";
|
@import "./views/voip/_DialPad.scss";
|
||||||
@import "./views/voip/_DialPadContextMenu.scss";
|
@import "./views/voip/_DialPadContextMenu.scss";
|
||||||
@import "./views/voip/_DialPadModal.scss";
|
@import "./views/voip/_DialPadModal.scss";
|
||||||
|
|
|
@ -199,120 +199,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_header {
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: left;
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callType {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_secondaryCallInfo {
|
|
||||||
&::before {
|
|
||||||
content: '·';
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_controls {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-color: $secondary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button_fullscreen {
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_button_expand {
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callInfo {
|
|
||||||
margin-left: 12px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_roomName {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: initial;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_secondaryCall_roomName {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callTypeSmall {
|
|
||||||
font-size: 12px;
|
|
||||||
color: $secondary-fg-color;
|
|
||||||
line-height: initial;
|
|
||||||
height: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_header_callTypeIcon {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 6px;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
background-color: $secondary-fg-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallView_header_callTypeIcon_voice::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_CallView_header_callTypeIcon_video::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallView_callControls {
|
.mx_CallView_callControls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
129
res/css/views/voip/_CallViewHeader.scss
Normal file
129
res/css/views/voip/_CallViewHeader.scss
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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_CallViewHeader {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callType {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_secondaryCallInfo {
|
||||||
|
&::before {
|
||||||
|
content: '·';
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_controls {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button_fullscreen {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_button_expand {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callInfo {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_roomName {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_secondaryCall_roomName {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callTypeSmall {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
line-height: initial;
|
||||||
|
height: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallViewHeader_callTypeIcon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 6px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_callTypeIcon_voice::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallViewHeader_callTypeIcon_video::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import CallView from "./CallView";
|
import CallView from "./CallView";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
@ -27,23 +27,8 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import UIStore from '../../../stores/UIStore';
|
|
||||||
import { lerp } from '../../../utils/AnimationUtils';
|
|
||||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
|
||||||
import { EventSubscription } from 'fbemitter';
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
import { PictureInPictureDragger } from './PictureInPictureDragger';
|
||||||
const PIP_VIEW_WIDTH = 336;
|
|
||||||
const PIP_VIEW_HEIGHT = 232;
|
|
||||||
|
|
||||||
const MOVING_AMT = 0.2;
|
|
||||||
const SNAPPING_AMT = 0.1;
|
|
||||||
|
|
||||||
const PADDING = {
|
|
||||||
top: 58,
|
|
||||||
bottom: 58,
|
|
||||||
left: 76,
|
|
||||||
right: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
|
@ -66,10 +51,6 @@ interface IState {
|
||||||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||||
// they belong to
|
// they belong to
|
||||||
secondaryCall: MatrixCall;
|
secondaryCall: MatrixCall;
|
||||||
|
|
||||||
// Position of the CallPreview
|
|
||||||
translationX: number;
|
|
||||||
translationY: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits a list of calls into one 'primary' one and a list
|
// Splits a list of calls into one 'primary' one and a list
|
||||||
|
@ -112,16 +93,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
private roomStoreToken: EventSubscription;
|
private roomStoreToken: EventSubscription;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private settingsWatcherRef: string;
|
private settingsWatcherRef: string;
|
||||||
private callViewWrapper = createRef<HTMLDivElement>();
|
|
||||||
private initX = 0;
|
|
||||||
private initY = 0;
|
|
||||||
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
|
||||||
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
|
|
||||||
private moving = false;
|
|
||||||
private scheduledUpdate = new MarkedExecution(
|
|
||||||
() => this.animationCallback(),
|
|
||||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -136,17 +107,12 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
roomId,
|
roomId,
|
||||||
primaryCall: primaryCall,
|
primaryCall: primaryCall,
|
||||||
secondaryCall: secondaryCalls[0],
|
secondaryCall: secondaryCalls[0],
|
||||||
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
|
||||||
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
document.addEventListener("mousemove", this.onMoving);
|
|
||||||
document.addEventListener("mouseup", this.onEndMoving);
|
|
||||||
window.addEventListener("resize", this.onResize);
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
}
|
}
|
||||||
|
@ -154,9 +120,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
document.removeEventListener("mousemove", this.onMoving);
|
|
||||||
document.removeEventListener("mouseup", this.onEndMoving);
|
|
||||||
window.removeEventListener("resize", this.onResize);
|
|
||||||
if (this.roomStoreToken) {
|
if (this.roomStoreToken) {
|
||||||
this.roomStoreToken.remove();
|
this.roomStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -164,94 +127,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onResize = (): void => {
|
|
||||||
this.snap(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
private animationCallback = () => {
|
|
||||||
// If the PiP isn't being dragged and there is only a tiny difference in
|
|
||||||
// the desiredTranslation and translation, quit the animationCallback
|
|
||||||
// loop. If that is the case, it means the PiP has snapped into its
|
|
||||||
// position and there is nothing to do. Not doing this would cause an
|
|
||||||
// infinite loop
|
|
||||||
if (
|
|
||||||
!this.moving &&
|
|
||||||
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
|
||||||
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
|
||||||
) return;
|
|
||||||
|
|
||||||
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
|
||||||
this.setState({
|
|
||||||
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
|
||||||
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
|
||||||
});
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
};
|
|
||||||
|
|
||||||
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
|
||||||
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
|
||||||
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
|
||||||
|
|
||||||
// Avoid overflow on the x axis
|
|
||||||
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
|
||||||
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
|
||||||
} else if (inTranslationX <= 0) {
|
|
||||||
this.desiredTranslationX = 0;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationX = inTranslationX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid overflow on the y axis
|
|
||||||
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
|
||||||
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
|
||||||
} else if (inTranslationY <= 0) {
|
|
||||||
this.desiredTranslationY = 0;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationY = inTranslationY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private snap(animate?: boolean): void {
|
|
||||||
const translationX = this.desiredTranslationX;
|
|
||||||
const translationY = this.desiredTranslationY;
|
|
||||||
// We subtract the PiP size from the window size in order to calculate
|
|
||||||
// the position to snap to from the PiP center and not its top-left
|
|
||||||
// corner
|
|
||||||
const windowWidth = (
|
|
||||||
UIStore.instance.windowWidth -
|
|
||||||
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
|
||||||
);
|
|
||||||
const windowHeight = (
|
|
||||||
UIStore.instance.windowHeight -
|
|
||||||
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
|
||||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
|
||||||
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = windowWidth - PADDING.right;
|
|
||||||
this.desiredTranslationY = PADDING.top;
|
|
||||||
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
|
||||||
this.desiredTranslationX = PADDING.left;
|
|
||||||
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
|
||||||
} else {
|
|
||||||
this.desiredTranslationX = PADDING.left;
|
|
||||||
this.desiredTranslationY = PADDING.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animate) {
|
|
||||||
// We start animating here because we want the PiP to move when we're
|
|
||||||
// resizing the window
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
translationX: this.desiredTranslationX,
|
|
||||||
translationY: this.desiredTranslationY,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomViewStoreUpdate = () => {
|
private onRoomViewStoreUpdate = () => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
|
||||||
|
@ -269,9 +144,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
// listen for call state changes to prod the render method, which
|
|
||||||
// may hide the global CallView if the call it is tracking is dead
|
|
||||||
case 'call_state': {
|
case 'call_state': {
|
||||||
|
// listen for call state changes to prod the render method, which
|
||||||
|
// may hide the global CallView if the call it is tracking is dead
|
||||||
|
|
||||||
this.updateCalls();
|
this.updateCalls();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -300,57 +176,26 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onStartMoving = (event: React.MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.moving = true;
|
|
||||||
this.initX = event.pageX - this.desiredTranslationX;
|
|
||||||
this.initY = event.pageY - this.desiredTranslationY;
|
|
||||||
this.scheduledUpdate.mark();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
|
||||||
if (!this.moving) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onEndMoving = () => {
|
|
||||||
this.moving = false;
|
|
||||||
this.snap(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const pipMode = true;
|
||||||
if (this.state.primaryCall) {
|
if (this.state.primaryCall) {
|
||||||
const translatePixelsX = this.state.translationX + "px";
|
|
||||||
const translatePixelsY = this.state.translationY + "px";
|
|
||||||
const style = {
|
|
||||||
transform: `translateX(${translatePixelsX})
|
|
||||||
translateY(${translatePixelsY})`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PictureInPictureDragger
|
||||||
className="mx_CallPreview"
|
className="mx_CallPreview"
|
||||||
style={style}
|
draggable={pipMode}
|
||||||
ref={this.callViewWrapper}
|
|
||||||
>
|
>
|
||||||
<CallView
|
{ ({ onStartMoving, onResize }) => <CallView
|
||||||
|
onMouseDownOnHeader={onStartMoving}
|
||||||
call={this.state.primaryCall}
|
call={this.state.primaryCall}
|
||||||
secondaryCall={this.state.secondaryCall}
|
secondaryCall={this.state.secondaryCall}
|
||||||
pipMode={true}
|
pipMode={pipMode}
|
||||||
onMouseDownOnHeader={this.onStartMoving}
|
onResize={onResize}
|
||||||
onResize={this.onResize}
|
/> }
|
||||||
/>
|
</PictureInPictureDragger>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PersistentApp />;
|
return <PersistentApp />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,28 +42,29 @@ import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||||
import CallViewSidebar from './CallViewSidebar';
|
import CallViewSidebar from './CallViewSidebar';
|
||||||
|
import { CallViewHeader } from './CallView/CallViewHeader';
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { Alignment } from "../elements/Tooltip";
|
import { Alignment } from "../elements/Tooltip";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// The call for us to display
|
// The call for us to display
|
||||||
call: MatrixCall;
|
call: MatrixCall;
|
||||||
|
|
||||||
// Another ongoing call to display information about
|
// Another ongoing call to display information about
|
||||||
secondaryCall?: MatrixCall;
|
secondaryCall?: MatrixCall;
|
||||||
|
|
||||||
// a callback which is called when the content in the CallView changes
|
// a callback which is called when the content in the CallView changes
|
||||||
// in a way that is likely to cause a resize.
|
// in a way that is likely to cause a resize.
|
||||||
onResize?: any;
|
onResize?: (event: Event) => void;
|
||||||
|
|
||||||
// Whether this call view is for picture-in-picture mode
|
// Whether this call view is for picture-in-picture mode
|
||||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||||
// This is sort of a proxy for a number of things but we currently have no
|
// This is sort of a proxy for a number of things but we currently have no
|
||||||
// need to control those things separately, so this is simpler.
|
// need to control those things separately, so this is simpler.
|
||||||
pipMode?: boolean;
|
pipMode?: boolean;
|
||||||
|
|
||||||
// Used for dragging the PiP CallView
|
// Used for dragging the PiP CallView
|
||||||
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
|
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -239,21 +240,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFullscreenClick = () => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'video_fullscreen',
|
|
||||||
fullscreen: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onExpandClick = () => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onControlsHideTimer = () => {
|
private onControlsHideTimer = () => {
|
||||||
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
||||||
this.controlsHideTimer = null;
|
this.controlsHideTimer = null;
|
||||||
|
@ -397,23 +383,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
this.setState({ hoveringControls: false });
|
this.setState({ hoveringControls: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomAvatarClick = (): void => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSecondaryRoomAvatarClick = (): void => {
|
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: userFacingRoomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCallResumeClick = (): void => {
|
private onCallResumeClick = (): void => {
|
||||||
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
|
||||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||||
|
@ -726,7 +695,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
let onHoldBackground = null;
|
let onHoldBackground = null;
|
||||||
const backgroundStyle: CSSProperties = {};
|
const backgroundStyle: CSSProperties = {};
|
||||||
const backgroundAvatarUrl = avatarUrlForMember(
|
const backgroundAvatarUrl = avatarUrlForMember(
|
||||||
// is it worth getting the size of the div to pass here?
|
// is it worth getting the size of the div to pass here?
|
||||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||||
);
|
);
|
||||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||||
|
@ -746,7 +715,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
mx_CallView_voice_hold: isOnHold,
|
mx_CallView_voice_hold: isOnHold,
|
||||||
});
|
});
|
||||||
|
|
||||||
contentView =(
|
contentView = (
|
||||||
<div className={classes} onMouseMove={this.onMouseMove}>
|
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||||
<div className="mx_CallView_voice_avatarsContainer">
|
<div className="mx_CallView_voice_avatarsContainer">
|
||||||
<div
|
<div
|
||||||
|
@ -848,83 +817,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
|
const myClassName = this.props.pipMode ? 'mx_CallView_pip' : 'mx_CallView_large';
|
||||||
let myClassName;
|
|
||||||
|
|
||||||
let fullScreenButton;
|
|
||||||
if (!this.props.pipMode) {
|
|
||||||
fullScreenButton = (
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
|
||||||
onClick={this.onFullscreenClick}
|
|
||||||
title={_t("Fill Screen")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let expandButton;
|
|
||||||
if (this.props.pipMode) {
|
|
||||||
expandButton = <AccessibleTooltipButton
|
|
||||||
className="mx_CallView_header_button mx_CallView_header_button_expand"
|
|
||||||
onClick={this.onExpandClick}
|
|
||||||
title={_t("Return to call")}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerControls = <div className="mx_CallView_header_controls">
|
|
||||||
{ fullScreenButton }
|
|
||||||
{ expandButton }
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
const callTypeIconClassName = classNames("mx_CallView_header_callTypeIcon", {
|
|
||||||
"mx_CallView_header_callTypeIcon_voice": !isVideoCall,
|
|
||||||
"mx_CallView_header_callTypeIcon_video": isVideoCall,
|
|
||||||
});
|
|
||||||
|
|
||||||
let header: React.ReactNode;
|
|
||||||
if (!this.props.pipMode) {
|
|
||||||
header = <div className="mx_CallView_header">
|
|
||||||
<div className={callTypeIconClassName} />
|
|
||||||
<span className="mx_CallView_header_callType">{ callTypeText }</span>
|
|
||||||
{ headerControls }
|
|
||||||
</div>;
|
|
||||||
myClassName = 'mx_CallView_large';
|
|
||||||
} else {
|
|
||||||
let secondaryCallInfo;
|
|
||||||
if (this.props.secondaryCall) {
|
|
||||||
secondaryCallInfo = <span className="mx_CallView_header_secondaryCallInfo">
|
|
||||||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
|
||||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
|
||||||
<span className="mx_CallView_secondaryCall_roomName">
|
|
||||||
{ _t("%(name)s on hold", { name: secCallRoom.name }) }
|
|
||||||
</span>
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
header = (
|
|
||||||
<div
|
|
||||||
className="mx_CallView_header"
|
|
||||||
onMouseDown={this.props.onMouseDownOnHeader}
|
|
||||||
>
|
|
||||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
|
||||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_CallView_header_callInfo">
|
|
||||||
<div className="mx_CallView_header_roomName">{ callRoom.name }</div>
|
|
||||||
<div className="mx_CallView_header_callTypeSmall">
|
|
||||||
{ callTypeText }
|
|
||||||
{ secondaryCallInfo }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{ headerControls }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
myClassName = 'mx_CallView_pip';
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={"mx_CallView " + myClassName}>
|
return <div className={"mx_CallView " + myClassName}>
|
||||||
{ header }
|
<CallViewHeader
|
||||||
|
onPipMouseDown={this.props.onMouseDownOnHeader}
|
||||||
|
pipMode={this.props.pipMode}
|
||||||
|
type={this.props.call.type}
|
||||||
|
callRooms={[callRoom, secCallRoom]}
|
||||||
|
/>
|
||||||
{ contentView }
|
{ contentView }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
132
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
132
src/components/views/voip/CallView/CallViewHeader.tsx
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 New Vector Ltd
|
||||||
|
|
||||||
|
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 { CallType } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import React from 'react';
|
||||||
|
import { _t } from '../../../../languageHandler';
|
||||||
|
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||||
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
|
import dis from '../../../../dispatcher/dispatcher';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||||
|
|
||||||
|
const callTypeTranslationByType: Record<CallType, string> = {
|
||||||
|
[CallType.Video]: _t("Video Call"),
|
||||||
|
[CallType.Voice]: _t("Voice Call"),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CallViewHeaderProps {
|
||||||
|
pipMode: boolean;
|
||||||
|
type: CallType;
|
||||||
|
callRooms?: Room[];
|
||||||
|
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRoomAvatarClick = (roomId: string) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFullscreenClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'video_fullscreen',
|
||||||
|
fullscreen: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExpandClick = (roomId: string) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type CallControlsProps = Pick<CallViewHeaderProps, 'pipMode' | 'type'> & {
|
||||||
|
roomId: string;
|
||||||
|
};
|
||||||
|
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ pipMode = false, type, roomId }) => {
|
||||||
|
return <div className="mx_CallViewHeader_controls">
|
||||||
|
{ (pipMode && type === CallType.Video) &&
|
||||||
|
<AccessibleTooltipButton className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
||||||
|
onClick={onFullscreenClick}
|
||||||
|
title={_t("Fill Screen")}
|
||||||
|
/> }
|
||||||
|
{ pipMode && <AccessibleTooltipButton className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
||||||
|
onClick={() => onExpandClick(roomId)}
|
||||||
|
title={_t("Return to call")}
|
||||||
|
/> }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
|
||||||
|
return <span className="mx_CallViewHeader_secondaryCallInfo">
|
||||||
|
<AccessibleButton element='span' onClick={() => onRoomAvatarClick(callRoom.roomId)}>
|
||||||
|
<RoomAvatar room={callRoom} height={16} width={16} />
|
||||||
|
<span className="mx_CallView_secondaryCall_roomName">
|
||||||
|
{ _t("%(name)s on hold", { name: callRoom.name }) }
|
||||||
|
</span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CallTypeIcon: React.FC<{ type: CallType }> = ({ type }) => {
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_CallViewHeader_callTypeIcon': true,
|
||||||
|
'mx_CallViewHeader_callTypeIcon_video': type === CallType.Video,
|
||||||
|
'mx_CallViewHeader_callTypeIcon_voice': type === CallType.Voice,
|
||||||
|
});
|
||||||
|
return <div className={classes} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||||
|
type,
|
||||||
|
pipMode = false,
|
||||||
|
callRooms = [],
|
||||||
|
onPipMouseDown,
|
||||||
|
}) => {
|
||||||
|
const [callRoom, onHoldCallRoom] = callRooms;
|
||||||
|
const callTypeText = callTypeTranslationByType[type];
|
||||||
|
const callRoomName = callRoom.name;
|
||||||
|
const { roomId } = callRoom;
|
||||||
|
|
||||||
|
if (!pipMode) {
|
||||||
|
return <div className="mx_CallViewHeader">
|
||||||
|
<CallTypeIcon type={type} />
|
||||||
|
<span className="mx_CallViewHeader_callType">{ callTypeText }</span>
|
||||||
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mx_CallViewHeader"
|
||||||
|
onMouseDown={onPipMouseDown}
|
||||||
|
>
|
||||||
|
<AccessibleButton onClick={() => onRoomAvatarClick(roomId)}>
|
||||||
|
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||||
|
</AccessibleButton>
|
||||||
|
<div className="mx_CallViewHeader_callInfo">
|
||||||
|
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
||||||
|
<div className="mx_CallViewHeader_callTypeSmall">
|
||||||
|
{ callTypeText }
|
||||||
|
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CallViewHeaderControls roomId={roomId} pipMode={pipMode} type={type} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
229
src/components/views/voip/PictureInPictureDragger.tsx
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 New Vector Ltd
|
||||||
|
|
||||||
|
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, { createRef } from 'react';
|
||||||
|
import UIStore from '../../../stores/UIStore';
|
||||||
|
import { lerp } from '../../../utils/AnimationUtils';
|
||||||
|
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||||
|
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
||||||
|
|
||||||
|
const PIP_VIEW_WIDTH = 336;
|
||||||
|
const PIP_VIEW_HEIGHT = 232;
|
||||||
|
|
||||||
|
const MOVING_AMT = 0.2;
|
||||||
|
const SNAPPING_AMT = 0.1;
|
||||||
|
|
||||||
|
const PADDING = {
|
||||||
|
top: 58,
|
||||||
|
bottom: 58,
|
||||||
|
left: 76,
|
||||||
|
right: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IChildrenOptions {
|
||||||
|
onStartMoving: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
onResize: (event: Event) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
children: ({ onStartMoving, onResize }: IChildrenOptions) => React.ReactNode;
|
||||||
|
draggable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// Position of the PictureInPictureDragger
|
||||||
|
translationX: number;
|
||||||
|
translationY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PictureInPictureDragger shows a small version of CallView 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.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.voip.PictureInPictureDragger")
|
||||||
|
export class PictureInPictureDragger extends React.Component<IProps, IState> {
|
||||||
|
private callViewWrapper = createRef<HTMLDivElement>();
|
||||||
|
private initX = 0;
|
||||||
|
private initY = 0;
|
||||||
|
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
|
||||||
|
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
|
||||||
|
private moving = false;
|
||||||
|
private scheduledUpdate = new MarkedExecution(
|
||||||
|
() => this.animationCallback(),
|
||||||
|
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
|
||||||
|
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
document.addEventListener("mousemove", this.onMoving);
|
||||||
|
document.addEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.addEventListener("resize", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousemove", this.onMoving);
|
||||||
|
document.removeEventListener("mouseup", this.onEndMoving);
|
||||||
|
window.removeEventListener("resize", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private animationCallback = () => {
|
||||||
|
// If the PiP isn't being dragged and there is only a tiny difference in
|
||||||
|
// the desiredTranslation and translation, quit the animationCallback
|
||||||
|
// loop. If that is the case, it means the PiP has snapped into its
|
||||||
|
// position and there is nothing to do. Not doing this would cause an
|
||||||
|
// infinite loop
|
||||||
|
if (
|
||||||
|
!this.moving &&
|
||||||
|
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
|
||||||
|
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
|
||||||
|
) return;
|
||||||
|
|
||||||
|
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
|
||||||
|
this.setState({
|
||||||
|
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
|
||||||
|
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
|
||||||
|
});
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
};
|
||||||
|
|
||||||
|
private setTranslation(inTranslationX: number, inTranslationY: number) {
|
||||||
|
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
|
||||||
|
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
|
||||||
|
|
||||||
|
// Avoid overflow on the x axis
|
||||||
|
if (inTranslationX + width >= UIStore.instance.windowWidth) {
|
||||||
|
this.desiredTranslationX = UIStore.instance.windowWidth - width;
|
||||||
|
} else if (inTranslationX <= 0) {
|
||||||
|
this.desiredTranslationX = 0;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationX = inTranslationX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid overflow on the y axis
|
||||||
|
if (inTranslationY + height >= UIStore.instance.windowHeight) {
|
||||||
|
this.desiredTranslationY = UIStore.instance.windowHeight - height;
|
||||||
|
} else if (inTranslationY <= 0) {
|
||||||
|
this.desiredTranslationY = 0;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationY = inTranslationY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResize = (): void => {
|
||||||
|
this.snap(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
private snap = (animate = false) => {
|
||||||
|
const translationX = this.desiredTranslationX;
|
||||||
|
const translationY = this.desiredTranslationY;
|
||||||
|
// We subtract the PiP size from the window size in order to calculate
|
||||||
|
// the position to snap to from the PiP center and not its top-left
|
||||||
|
// corner
|
||||||
|
const windowWidth = (
|
||||||
|
UIStore.instance.windowWidth -
|
||||||
|
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
|
||||||
|
);
|
||||||
|
const windowHeight = (
|
||||||
|
UIStore.instance.windowHeight -
|
||||||
|
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||||
|
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||||
|
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = windowWidth - PADDING.right;
|
||||||
|
this.desiredTranslationY = PADDING.top;
|
||||||
|
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
|
||||||
|
this.desiredTranslationX = PADDING.left;
|
||||||
|
this.desiredTranslationY = windowHeight - PADDING.bottom;
|
||||||
|
} else {
|
||||||
|
this.desiredTranslationX = PADDING.left;
|
||||||
|
this.desiredTranslationY = PADDING.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We start animating here because we want the PiP to move when we're
|
||||||
|
// resizing the window
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
// We start animating here because we want the PiP to move when we're
|
||||||
|
// resizing the window
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
translationX: this.desiredTranslationX,
|
||||||
|
translationY: this.desiredTranslationY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onStartMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.moving = true;
|
||||||
|
this.initX = event.pageX - this.desiredTranslationX;
|
||||||
|
this.initY = event.pageY - this.desiredTranslationY;
|
||||||
|
this.scheduledUpdate.mark();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMoving = (event: React.MouseEvent | MouseEvent) => {
|
||||||
|
if (!this.moving) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEndMoving = () => {
|
||||||
|
this.moving = false;
|
||||||
|
this.snap(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const translatePixelsX = this.state.translationX + "px";
|
||||||
|
const translatePixelsY = this.state.translationY + "px";
|
||||||
|
const style = {
|
||||||
|
transform: `translateX(${translatePixelsX})
|
||||||
|
translateY(${translatePixelsY})`,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={this.props.className}
|
||||||
|
style={this.props.draggable ? style : undefined}
|
||||||
|
ref={this.callViewWrapper}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{ this.props.children({
|
||||||
|
onStartMoving: this.onStartMoving,
|
||||||
|
onResize: this.onResize,
|
||||||
|
}) }
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue