Calling: Picture-in-picture
This commit is contained in:
parent
7b15bddfc9
commit
a581f6ea81
13 changed files with 467 additions and 3 deletions
|
@ -2863,6 +2863,14 @@
|
||||||
"message": "Settings",
|
"message": "Settings",
|
||||||
"description": "Title for device selection settings"
|
"description": "Title for device selection settings"
|
||||||
},
|
},
|
||||||
|
"calling__pip": {
|
||||||
|
"message": "Picture-in-picture",
|
||||||
|
"description": "Title for picture-in-picture toggle"
|
||||||
|
},
|
||||||
|
"calling__hangup": {
|
||||||
|
"message": "Hang Up",
|
||||||
|
"description": "Title for hang up button"
|
||||||
|
},
|
||||||
"callingDeviceSelection__label--video": {
|
"callingDeviceSelection__label--video": {
|
||||||
"message": "Video",
|
"message": "Video",
|
||||||
"description": "Label for video input selector"
|
"description": "Label for video input selector"
|
||||||
|
|
1
images/icons/v2/collapse-24.svg
Normal file
1
images/icons/v2/collapse-24.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>collapse-24</title><polygon points="15.38 9.68 21.53 3.53 20.47 2.47 14.32 8.62 14.5 7.52 14.5 3 13 3 13 11 21 11 21 9.5 16.48 9.5 15.38 9.68"/><polygon points="3 13 3 14.5 7.52 14.5 8.62 14.32 2.47 20.47 3.53 21.53 9.68 15.38 9.5 16.48 9.5 21 11 21 11 13 3 13"/></svg>
|
After Width: | Height: | Size: 359 B |
1
images/icons/v2/expand-24.svg
Normal file
1
images/icons/v2/expand-24.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>expand-24</title><polygon points="14 2 14 3.5 18.52 3.5 19.62 3.32 13.47 9.47 14.53 10.53 20.68 4.38 20.5 5.48 20.5 10 22 10 22 2 14 2"/><polygon points="4.38 20.68 10.53 14.53 9.47 13.47 3.32 19.62 3.5 18.52 3.5 14 2 14 2 22 10 22 10 20.5 5.48 20.5 4.38 20.68"/></svg>
|
After Width: | Height: | Size: 359 B |
|
@ -84,3 +84,7 @@
|
||||||
font-size: large;
|
font-size: large;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.call-manager-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
|
@ -6133,7 +6133,7 @@ button.module-image__border-overlay:focus {
|
||||||
.module-ongoing-call__settings {
|
.module-ongoing-call__settings {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 25px;
|
top: 25px;
|
||||||
right: 25px;
|
right: 65px;
|
||||||
|
|
||||||
&--button {
|
&--button {
|
||||||
@include color-svg(
|
@include color-svg(
|
||||||
|
@ -6145,6 +6145,115 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-ongoing-call__pip {
|
||||||
|
position: absolute;
|
||||||
|
top: 25px;
|
||||||
|
right: 25px;
|
||||||
|
|
||||||
|
&--button {
|
||||||
|
@include color-svg('../images/icons/v2/collapse-24.svg', $color-white);
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-calling-pip {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
background-color: $color-gray-95;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.05), 0px 8px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
cursor: grab;
|
||||||
|
height: 158px;
|
||||||
|
position: absolute;
|
||||||
|
width: 120px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&__video {
|
||||||
|
&--remote {
|
||||||
|
align-items: center;
|
||||||
|
background-color: $color-gray-95;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
display: flex;
|
||||||
|
height: 120px;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--local {
|
||||||
|
bottom: 38px;
|
||||||
|
height: 32px;
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--background {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blur {
|
||||||
|
backdrop-filter: blur(7px);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
background-color: $color-black-alpha-40;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--avatar img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
align-items: center;
|
||||||
|
background-color: $color-gray-02;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 38px;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
&--hangup {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/phone-down-28.svg',
|
||||||
|
$color-gray-75
|
||||||
|
);
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/phone-down-28.svg',
|
||||||
|
$color-gray-15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pip {
|
||||||
|
@include color-svg('../images/icons/v2/expand-24.svg', $color-gray-75);
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/expand-24.svg', $color-gray-15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Module: Left Pane
|
// Module: Left Pane
|
||||||
|
|
||||||
.module-left-pane {
|
.module-left-pane {
|
||||||
|
|
|
@ -33,12 +33,14 @@ const defaultProps = {
|
||||||
hasLocalVideo: true,
|
hasLocalVideo: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
i18n,
|
i18n,
|
||||||
|
pip: false,
|
||||||
renderDeviceSelection: () => <div />,
|
renderDeviceSelection: () => <div />,
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
setLocalPreview: action('set-local-preview'),
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
|
togglePip: action('toggle-pip'),
|
||||||
toggleSettings: action('toggle-settings'),
|
toggleSettings: action('toggle-settings'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { CallingPip } from './CallingPip';
|
||||||
import { CallScreen, PropsType as CallScreenPropsType } from './CallScreen';
|
import { CallScreen, PropsType as CallScreenPropsType } from './CallScreen';
|
||||||
import {
|
import {
|
||||||
IncomingCallBar,
|
IncomingCallBar,
|
||||||
|
@ -10,6 +11,7 @@ import { CallDetailsType } from '../state/ducks/calling';
|
||||||
type CallManagerPropsType = {
|
type CallManagerPropsType = {
|
||||||
callDetails?: CallDetailsType;
|
callDetails?: CallDetailsType;
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
|
pip: boolean;
|
||||||
renderDeviceSelection: () => JSX.Element;
|
renderDeviceSelection: () => JSX.Element;
|
||||||
settingsDialogOpen: boolean;
|
settingsDialogOpen: boolean;
|
||||||
};
|
};
|
||||||
|
@ -28,12 +30,14 @@ export const CallManager = ({
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
hasRemoteVideo,
|
hasRemoteVideo,
|
||||||
i18n,
|
i18n,
|
||||||
|
pip,
|
||||||
renderDeviceSelection,
|
renderDeviceSelection,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
settingsDialogOpen,
|
settingsDialogOpen,
|
||||||
|
togglePip,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
}: PropsType): JSX.Element | null => {
|
}: PropsType): JSX.Element | null => {
|
||||||
if (!callDetails || !callState) {
|
if (!callDetails || !callState) {
|
||||||
|
@ -46,6 +50,21 @@ export const CallManager = ({
|
||||||
const ringing = callState === CallState.Ringing;
|
const ringing = callState === CallState.Ringing;
|
||||||
|
|
||||||
if (outgoing || ongoing) {
|
if (outgoing || ongoing) {
|
||||||
|
if (pip) {
|
||||||
|
return (
|
||||||
|
<CallingPip
|
||||||
|
callDetails={callDetails}
|
||||||
|
hangUp={hangUp}
|
||||||
|
hasLocalVideo={hasLocalVideo}
|
||||||
|
hasRemoteVideo={hasRemoteVideo}
|
||||||
|
i18n={i18n}
|
||||||
|
setLocalPreview={setLocalPreview}
|
||||||
|
setRendererCanvas={setRendererCanvas}
|
||||||
|
togglePip={togglePip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CallScreen
|
<CallScreen
|
||||||
|
@ -60,6 +79,7 @@ export const CallManager = ({
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
setLocalAudio={setLocalAudio}
|
setLocalAudio={setLocalAudio}
|
||||||
setLocalVideo={setLocalVideo}
|
setLocalVideo={setLocalVideo}
|
||||||
|
togglePip={togglePip}
|
||||||
toggleSettings={toggleSettings}
|
toggleSettings={toggleSettings}
|
||||||
/>
|
/>
|
||||||
{settingsDialogOpen && renderDeviceSelection()}
|
{settingsDialogOpen && renderDeviceSelection()}
|
||||||
|
|
|
@ -36,6 +36,7 @@ const defaultProps = {
|
||||||
setLocalPreview: action('set-local-preview'),
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
|
togglePip: action('toggle-pip'),
|
||||||
toggleSettings: action('toggle-settings'),
|
toggleSettings: action('toggle-settings'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ export type PropsType = {
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
|
togglePip: () => void;
|
||||||
toggleSettings: () => void;
|
toggleSettings: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -209,6 +210,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
hasRemoteVideo,
|
hasRemoteVideo,
|
||||||
i18n,
|
i18n,
|
||||||
|
togglePip,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showControls } = this.state;
|
const { showControls } = this.state;
|
||||||
|
@ -256,6 +258,14 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
||||||
onClick={toggleSettings}
|
onClick={toggleSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="module-ongoing-call__pip">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={i18n('calling__pip')}
|
||||||
|
className="module-ongoing-call__pip--button"
|
||||||
|
onClick={togglePip}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasRemoteVideo
|
{hasRemoteVideo
|
||||||
? this.renderRemoteVideo()
|
? this.renderRemoteVideo()
|
||||||
|
|
45
ts/components/CallingPip.stories.tsx
Normal file
45
ts/components/CallingPip.stories.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { boolean } from '@storybook/addon-knobs';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { ColorType } from '../types/Colors';
|
||||||
|
import { CallingPip, PropsType } from './CallingPip';
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const callDetails = {
|
||||||
|
callId: 0,
|
||||||
|
isIncoming: true,
|
||||||
|
isVideoCall: true,
|
||||||
|
|
||||||
|
avatarPath: undefined,
|
||||||
|
color: 'ultramarine' as ColorType,
|
||||||
|
title: 'Rick Sanchez',
|
||||||
|
name: 'Rick Sanchez',
|
||||||
|
phoneNumber: '3051234567',
|
||||||
|
profileName: 'Rick Sanchez',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
callDetails,
|
||||||
|
hangUp: action('hang-up'),
|
||||||
|
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||||
|
hasRemoteVideo: boolean(
|
||||||
|
'hasRemoteVideo',
|
||||||
|
overrideProps.hasRemoteVideo || false
|
||||||
|
),
|
||||||
|
i18n,
|
||||||
|
setLocalPreview: action('set-local-preview'),
|
||||||
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
|
togglePip: action('toggle-pip'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const story = storiesOf('Components/CallingPip', module);
|
||||||
|
|
||||||
|
story.add('Default', () => {
|
||||||
|
const props = createProps();
|
||||||
|
return <CallingPip {...props} />;
|
||||||
|
});
|
241
ts/components/CallingPip.tsx
Normal file
241
ts/components/CallingPip.tsx
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
CallDetailsType,
|
||||||
|
HangUpType,
|
||||||
|
SetLocalPreviewType,
|
||||||
|
SetRendererCanvasType,
|
||||||
|
} from '../state/ducks/calling';
|
||||||
|
import { Avatar } from './Avatar';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
function renderAvatar(
|
||||||
|
callDetails: CallDetailsType,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): JSX.Element {
|
||||||
|
const {
|
||||||
|
avatarPath,
|
||||||
|
color,
|
||||||
|
name,
|
||||||
|
phoneNumber,
|
||||||
|
profileName,
|
||||||
|
title,
|
||||||
|
} = callDetails;
|
||||||
|
|
||||||
|
const backgroundStyle = avatarPath
|
||||||
|
? {
|
||||||
|
backgroundImage: `url("${avatarPath}")`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
backgroundColor: color,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-calling-pip__video--remote">
|
||||||
|
<div
|
||||||
|
className="module-calling-pip__video--background"
|
||||||
|
style={backgroundStyle}
|
||||||
|
/>
|
||||||
|
<div className="module-calling-pip__video--blur" />
|
||||||
|
<div className="module-calling-pip__video--avatar">
|
||||||
|
<Avatar
|
||||||
|
avatarPath={avatarPath}
|
||||||
|
color={color || 'ultramarine'}
|
||||||
|
noteToSelf={false}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
name={name}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
profileName={profileName}
|
||||||
|
title={title}
|
||||||
|
size={52}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
callDetails: CallDetailsType;
|
||||||
|
hangUp: (_: HangUpType) => void;
|
||||||
|
hasLocalVideo: boolean;
|
||||||
|
hasRemoteVideo: boolean;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
|
togglePip: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PIP_HEIGHT = 156;
|
||||||
|
const PIP_WIDTH = 120;
|
||||||
|
const PIP_DEFAULT_Y = 56;
|
||||||
|
const PIP_PADDING = 8;
|
||||||
|
|
||||||
|
export const CallingPip = ({
|
||||||
|
callDetails,
|
||||||
|
hangUp,
|
||||||
|
hasLocalVideo,
|
||||||
|
hasRemoteVideo,
|
||||||
|
i18n,
|
||||||
|
setLocalPreview,
|
||||||
|
setRendererCanvas,
|
||||||
|
togglePip,
|
||||||
|
}: PropsType): JSX.Element | null => {
|
||||||
|
const videoContainerRef = React.useRef(null);
|
||||||
|
const localVideoRef = React.useRef(null);
|
||||||
|
const remoteVideoRef = React.useRef(null);
|
||||||
|
|
||||||
|
const [dragState, setDragState] = React.useState({
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
isDragging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [dragContainerStyle, setDragContainerStyle] = React.useState({
|
||||||
|
translateX: window.innerWidth - PIP_WIDTH - PIP_PADDING,
|
||||||
|
translateY: PIP_DEFAULT_Y,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setLocalPreview({ element: localVideoRef });
|
||||||
|
setRendererCanvas({ element: remoteVideoRef });
|
||||||
|
}, [setLocalPreview, setRendererCanvas]);
|
||||||
|
|
||||||
|
const handleMouseMove = React.useCallback(
|
||||||
|
(ev: MouseEvent) => {
|
||||||
|
if (dragState.isDragging) {
|
||||||
|
setDragContainerStyle({
|
||||||
|
translateX: ev.clientX - dragState.offsetX,
|
||||||
|
translateY: ev.clientY - dragState.offsetY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dragState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = React.useCallback(() => {
|
||||||
|
if (dragState.isDragging) {
|
||||||
|
const { translateX, translateY } = dragContainerStyle;
|
||||||
|
const { innerHeight, innerWidth } = window;
|
||||||
|
|
||||||
|
const proximityRatio: Record<string, number> = {
|
||||||
|
top: translateY / innerHeight,
|
||||||
|
right: (innerWidth - translateX) / innerWidth,
|
||||||
|
bottom: (innerHeight - translateY) / innerHeight,
|
||||||
|
left: translateX / innerWidth,
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapTo = Object.keys(proximityRatio).reduce(
|
||||||
|
(minKey: string, key: string): string => {
|
||||||
|
return proximityRatio[key] < proximityRatio[minKey] ? key : minKey;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setDragState({
|
||||||
|
...dragState,
|
||||||
|
isDragging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextX = Math.max(
|
||||||
|
PIP_PADDING,
|
||||||
|
Math.min(translateX, innerWidth - PIP_WIDTH - PIP_PADDING)
|
||||||
|
);
|
||||||
|
let nextY = Math.max(
|
||||||
|
PIP_DEFAULT_Y,
|
||||||
|
Math.min(translateY, innerHeight - PIP_HEIGHT - PIP_PADDING)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snapTo === 'top') {
|
||||||
|
nextY = PIP_DEFAULT_Y;
|
||||||
|
}
|
||||||
|
if (snapTo === 'right') {
|
||||||
|
nextX = innerWidth - PIP_WIDTH - PIP_PADDING;
|
||||||
|
}
|
||||||
|
if (snapTo === 'bottom') {
|
||||||
|
nextY = innerHeight - PIP_HEIGHT - PIP_PADDING;
|
||||||
|
}
|
||||||
|
if (snapTo === 'left') {
|
||||||
|
nextX = PIP_PADDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragContainerStyle({
|
||||||
|
translateX: nextX,
|
||||||
|
translateY: nextY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [dragState, dragContainerStyle]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (dragState.isDragging) {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove, false);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp, false);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp, false);
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp, false);
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove, false);
|
||||||
|
};
|
||||||
|
}, [dragState, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||||
|
<div
|
||||||
|
className="module-calling-pip"
|
||||||
|
onMouseDown={ev => {
|
||||||
|
const node = videoContainerRef.current;
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = (node as HTMLElement).getBoundingClientRect();
|
||||||
|
const offsetX = ev.clientX - rect.left;
|
||||||
|
const offsetY = ev.clientY - rect.top;
|
||||||
|
|
||||||
|
setDragState({
|
||||||
|
isDragging: true,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
ref={videoContainerRef}
|
||||||
|
style={{
|
||||||
|
cursor: dragState.isDragging ? '-webkit-grabbing' : '-webkit-grab',
|
||||||
|
transform: `translate3d(${dragContainerStyle.translateX}px,${dragContainerStyle.translateY}px, 0)`,
|
||||||
|
transition: dragState.isDragging ? 'none' : 'transform ease-out 300ms',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasRemoteVideo ? (
|
||||||
|
<canvas
|
||||||
|
className="module-calling-pip__video--remote"
|
||||||
|
ref={remoteVideoRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
renderAvatar(callDetails, i18n)
|
||||||
|
)}
|
||||||
|
{hasLocalVideo ? (
|
||||||
|
<video
|
||||||
|
className="module-calling-pip__video--local"
|
||||||
|
ref={localVideoRef}
|
||||||
|
autoPlay
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="module-calling-pip__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={i18n('calling__hangup')}
|
||||||
|
className="module-calling-pip__button--hangup"
|
||||||
|
onClick={() => {
|
||||||
|
hangUp({ callId: callDetails.callId });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={i18n('calling__pip')}
|
||||||
|
className="module-calling-pip__button--pip"
|
||||||
|
onClick={togglePip}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -38,6 +38,7 @@ export type CallingStateType = MediaDeviceSettings & {
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
pip: boolean;
|
||||||
settingsDialogOpen: boolean;
|
settingsDialogOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,6 +106,7 @@ const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
const SET_LOCAL_AUDIO = 'calling/SET_LOCAL_AUDIO';
|
const SET_LOCAL_AUDIO = 'calling/SET_LOCAL_AUDIO';
|
||||||
const SET_LOCAL_VIDEO = 'calling/SET_LOCAL_VIDEO';
|
const SET_LOCAL_VIDEO = 'calling/SET_LOCAL_VIDEO';
|
||||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||||
|
const TOGGLE_PIP = 'calling/TOGGLE_PIP';
|
||||||
const TOGGLE_SETTINGS = 'calling/TOGGLE_SETTINGS';
|
const TOGGLE_SETTINGS = 'calling/TOGGLE_SETTINGS';
|
||||||
|
|
||||||
type AcceptCallActionType = {
|
type AcceptCallActionType = {
|
||||||
|
@ -177,6 +179,10 @@ type SetLocalVideoFulfilledActionType = {
|
||||||
payload: SetLocalVideoType;
|
payload: SetLocalVideoType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TogglePipActionType = {
|
||||||
|
type: 'calling/TOGGLE_PIP';
|
||||||
|
};
|
||||||
|
|
||||||
type ToggleSettingsActionType = {
|
type ToggleSettingsActionType = {
|
||||||
type: 'calling/TOGGLE_SETTINGS';
|
type: 'calling/TOGGLE_SETTINGS';
|
||||||
};
|
};
|
||||||
|
@ -196,6 +202,7 @@ export type CallingActionType =
|
||||||
| SetLocalAudioActionType
|
| SetLocalAudioActionType
|
||||||
| SetLocalVideoActionType
|
| SetLocalVideoActionType
|
||||||
| SetLocalVideoFulfilledActionType
|
| SetLocalVideoFulfilledActionType
|
||||||
|
| TogglePipActionType
|
||||||
| ToggleSettingsActionType;
|
| ToggleSettingsActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -376,6 +383,12 @@ function setLocalVideo(payload: SetLocalVideoType): SetLocalVideoActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function togglePip(): TogglePipActionType {
|
||||||
|
return {
|
||||||
|
type: TOGGLE_PIP,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSettings(): ToggleSettingsActionType {
|
function toggleSettings(): ToggleSettingsActionType {
|
||||||
return {
|
return {
|
||||||
type: TOGGLE_SETTINGS,
|
type: TOGGLE_SETTINGS,
|
||||||
|
@ -410,6 +423,7 @@ export const actions = {
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
|
togglePip,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -427,6 +441,7 @@ function getEmptyState(): CallingStateType {
|
||||||
hasLocalAudio: false,
|
hasLocalAudio: false,
|
||||||
hasLocalVideo: false,
|
hasLocalVideo: false,
|
||||||
hasRemoteVideo: false,
|
hasRemoteVideo: false,
|
||||||
|
pip: false,
|
||||||
selectedCamera: undefined,
|
selectedCamera: undefined,
|
||||||
selectedMicrophone: undefined,
|
selectedMicrophone: undefined,
|
||||||
selectedSpeaker: undefined,
|
selectedSpeaker: undefined,
|
||||||
|
@ -545,5 +560,12 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === TOGGLE_PIP) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pip: !state.pip,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12863,7 +12863,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/CallScreen.tsx",
|
"path": "ts/components/CallScreen.tsx",
|
||||||
"line": " this.localVideoRef = React.createRef();",
|
"line": " this.localVideoRef = React.createRef();",
|
||||||
"lineNumber": 79,
|
"lineNumber": 80,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-06-02T21:51:34.813Z",
|
"updated": "2020-06-02T21:51:34.813Z",
|
||||||
"reasonDetail": "Used to render local preview video"
|
"reasonDetail": "Used to render local preview video"
|
||||||
|
@ -12872,7 +12872,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/CallScreen.tsx",
|
"path": "ts/components/CallScreen.tsx",
|
||||||
"line": " this.remoteVideoRef = React.createRef();",
|
"line": " this.remoteVideoRef = React.createRef();",
|
||||||
"lineNumber": 80,
|
"lineNumber": 81,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-09-14T23:03:44.863Z"
|
"updated": "2020-09-14T23:03:44.863Z"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue