247 lines
6.6 KiB
TypeScript
247 lines
6.6 KiB
TypeScript
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import React from 'react';
|
|
import {
|
|
HangUpType,
|
|
SetLocalPreviewType,
|
|
SetRendererCanvasType,
|
|
} from '../state/ducks/calling';
|
|
import { Avatar } from './Avatar';
|
|
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
|
import { ColorType } from '../types/Colors';
|
|
import { LocalizerType } from '../types/Util';
|
|
|
|
function renderAvatar(
|
|
{
|
|
avatarPath,
|
|
color,
|
|
name,
|
|
phoneNumber,
|
|
profileName,
|
|
title,
|
|
}: {
|
|
avatarPath?: string;
|
|
color?: ColorType;
|
|
title: string;
|
|
name?: string;
|
|
phoneNumber?: string;
|
|
profileName?: string;
|
|
},
|
|
i18n: LocalizerType
|
|
): JSX.Element {
|
|
return (
|
|
<div className="module-calling-pip__video--remote">
|
|
<CallBackgroundBlur avatarPath={avatarPath} color={color}>
|
|
<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>
|
|
</CallBackgroundBlur>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type PropsType = {
|
|
conversation: {
|
|
id: string;
|
|
avatarPath?: string;
|
|
color?: ColorType;
|
|
title: string;
|
|
name?: string;
|
|
phoneNumber?: string;
|
|
profileName?: string;
|
|
};
|
|
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 = ({
|
|
conversation,
|
|
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(conversation, 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({ conversationId: conversation.id });
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
aria-label={i18n('calling__pip')}
|
|
className="module-calling-pip__button--pip"
|
|
onClick={togglePip}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|