// 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 = { 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
{ 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', }} > {hasLocalVideo ? (
); };