signal-desktop/ts/components/CallingPip.tsx

206 lines
5.8 KiB
TypeScript
Raw Normal View History

2020-10-30 20:34:04 +00:00
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
2020-10-01 00:43:05 +00:00
import React from 'react';
2020-11-19 18:11:35 +00:00
import { Tooltip } from './Tooltip';
2020-11-17 15:07:53 +00:00
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
import { LocalizerType } from '../types/Util';
import { ConversationType } from '../state/ducks/conversations';
2020-11-17 19:49:48 +00:00
import { VideoFrameSource } from '../types/Calling';
2020-10-01 00:43:05 +00:00
import {
2020-11-17 15:07:53 +00:00
DirectCallStateType,
GroupCallStateType,
2020-10-01 00:43:05 +00:00
HangUpType,
SetLocalPreviewType,
SetRendererCanvasType,
} from '../state/ducks/calling';
export type PropsType = {
2020-11-17 15:07:53 +00:00
call: DirectCallStateType | GroupCallStateType;
conversation: ConversationType;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
2020-10-01 00:43:05 +00:00
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 = ({
2020-11-17 15:07:53 +00:00
call,
conversation,
2020-11-17 15:07:53 +00:00
getGroupCallVideoFrameSource,
2020-10-01 00:43:05 +00:00
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 });
2020-11-17 15:07:53 +00:00
}, [setLocalPreview]);
2020-10-01 00:43:05 +00:00
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',
}}
>
2020-11-17 15:07:53 +00:00
<CallingPipRemoteVideo
call={call}
conversation={conversation}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
setRendererCanvas={setRendererCanvas}
/>
2020-10-01 00:43:05 +00:00
{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: conversation.id });
2020-10-01 00:43:05 +00:00
}}
type="button"
2020-11-19 18:11:35 +00:00
/>
<Tooltip content={i18n('calling__pip--off')}>
<button
aria-label={i18n('calling__pip--off')}
className="module-calling-pip__button--pip"
onClick={togglePip}
type="button"
2020-11-17 15:07:53 +00:00
>
<div />
2020-11-19 18:11:35 +00:00
</button>
</Tooltip>
2020-10-01 00:43:05 +00:00
</div>
</div>
);
};