signal-desktop/ts/components/CallingPip.tsx
2020-11-23 14:49:32 -05:00

197 lines
5.5 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
import { LocalizerType } from '../types/Util';
import { VideoFrameSource } from '../types/Calling';
import {
ActiveCallType,
HangUpType,
SetLocalPreviewType,
SetRendererCanvasType,
} from '../state/ducks/calling';
export type PropsType = {
activeCall: ActiveCallType;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
hangUp: (_: HangUpType) => void;
hasLocalVideo: 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 = ({
activeCall,
getGroupCallVideoFrameSource,
hangUp,
hasLocalVideo,
i18n,
setLocalPreview,
setRendererCanvas,
togglePip,
}: PropsType): JSX.Element | null => {
const videoContainerRef = React.useRef(null);
const localVideoRef = 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 });
}, [setLocalPreview]);
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',
}}
>
<CallingPipRemoteVideo
activeCall={activeCall}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
setRendererCanvas={setRendererCanvas}
/>
{hasLocalVideo ? (
<video
className="module-calling-pip__video--local"
ref={localVideoRef}
autoPlay
/>
) : null}
<div className="module-calling-pip__actions">
<button
aria-label={i18n('calling__hangup')}
className="module-calling-pip__button--hangup"
onClick={() => {
hangUp({ conversationId: activeCall.conversation.id });
}}
type="button"
/>
<button
aria-label={i18n('calling__pip--off')}
className="module-calling-pip__button--pip"
onClick={togglePip}
type="button"
>
<div />
</button>
</div>
</div>
);
};