signal-desktop/ts/components/CallScreen.tsx

357 lines
9 KiB
TypeScript
Raw Normal View History

2020-06-04 11:16:19 -07:00
import React from 'react';
import classNames from 'classnames';
import {
CallDetailsType,
HangUpType,
SetLocalAudioType,
2020-08-26 20:03:42 -04:00
SetLocalPreviewType,
2020-06-04 11:16:19 -07:00
SetLocalVideoType,
2020-08-26 20:03:42 -04:00
SetRendererCanvasType,
2020-06-04 11:16:19 -07:00
} from '../state/ducks/calling';
import { Avatar } from './Avatar';
2020-10-07 21:25:33 -04:00
import { CallingButton, CallingButtonType } from './CallingButton';
2020-06-04 11:16:19 -07:00
import { CallState } from '../types/Calling';
import { LocalizerType } from '../types/Util';
export type PropsType = {
callDetails?: CallDetailsType;
callState?: CallState;
hangUp: (_: HangUpType) => void;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
hasRemoteVideo: boolean;
i18n: LocalizerType;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
2020-08-26 20:03:42 -04:00
setLocalPreview: (_: SetLocalPreviewType) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
2020-09-30 20:43:05 -04:00
togglePip: () => void;
2020-08-26 20:03:42 -04:00
toggleSettings: () => void;
2020-06-04 11:16:19 -07:00
};
type StateType = {
acceptedDuration: number | null;
showControls: boolean;
};
export class CallScreen extends React.Component<PropsType, StateType> {
private interval: NodeJS.Timeout | null;
2020-09-11 17:46:52 -07:00
private controlsFadeTimer: NodeJS.Timeout | null;
2020-09-11 17:46:52 -07:00
2020-06-04 11:16:19 -07:00
private readonly localVideoRef: React.RefObject<HTMLVideoElement>;
2020-09-11 17:46:52 -07:00
2020-06-04 11:16:19 -07:00
private readonly remoteVideoRef: React.RefObject<HTMLCanvasElement>;
constructor(props: PropsType) {
super(props);
this.state = {
acceptedDuration: null,
showControls: true,
};
this.interval = null;
this.controlsFadeTimer = null;
this.localVideoRef = React.createRef();
this.remoteVideoRef = React.createRef();
}
2020-09-11 17:46:52 -07:00
public componentDidMount(): void {
const { setLocalPreview, setRendererCanvas } = this.props;
2020-06-04 11:16:19 -07:00
// It's really jump with a value of 500ms.
this.interval = setInterval(this.updateAcceptedTimer, 100);
this.fadeControls();
document.addEventListener('keydown', this.handleKeyDown);
2020-08-26 20:03:42 -04:00
2020-09-11 17:46:52 -07:00
setLocalPreview({ element: this.localVideoRef });
setRendererCanvas({ element: this.remoteVideoRef });
2020-06-04 11:16:19 -07:00
}
2020-09-11 17:46:52 -07:00
public componentWillUnmount(): void {
const { setLocalPreview, setRendererCanvas } = this.props;
2020-06-04 11:16:19 -07:00
document.removeEventListener('keydown', this.handleKeyDown);
if (this.interval) {
clearInterval(this.interval);
}
if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer);
}
2020-09-11 17:46:52 -07:00
setLocalPreview({ element: undefined });
setRendererCanvas({ element: undefined });
2020-06-04 11:16:19 -07:00
}
2020-09-11 17:46:52 -07:00
updateAcceptedTimer = (): void => {
const { callDetails } = this.props;
2020-06-04 11:16:19 -07:00
if (!callDetails) {
return;
}
if (callDetails.acceptedTime) {
2020-06-04 11:16:19 -07:00
this.setState({
acceptedDuration: Date.now() - callDetails.acceptedTime,
2020-06-04 11:16:19 -07:00
});
}
};
2020-09-11 17:46:52 -07:00
handleKeyDown = (event: KeyboardEvent): void => {
2020-06-04 11:16:19 -07:00
const { callDetails } = this.props;
if (!callDetails) {
return;
}
let eventHandled = false;
2020-10-07 21:25:33 -04:00
if (event.shiftKey && (event.key === 'V' || event.key === 'v')) {
2020-06-04 11:16:19 -07:00
this.toggleVideo();
eventHandled = true;
2020-10-07 21:25:33 -04:00
} else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) {
2020-06-04 11:16:19 -07:00
this.toggleAudio();
eventHandled = true;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
this.showControls();
}
};
2020-09-11 17:46:52 -07:00
showControls = (): void => {
const { showControls } = this.state;
if (!showControls) {
2020-06-04 11:16:19 -07:00
this.setState({
showControls: true,
});
}
this.fadeControls();
};
2020-09-11 17:46:52 -07:00
fadeControls = (): void => {
2020-06-04 11:16:19 -07:00
if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer);
}
this.controlsFadeTimer = setTimeout(() => {
this.setState({
showControls: false,
});
}, 5000);
};
2020-09-11 17:46:52 -07:00
toggleAudio = (): void => {
2020-06-04 11:16:19 -07:00
const { callDetails, hasLocalAudio, setLocalAudio } = this.props;
if (!callDetails) {
return;
}
setLocalAudio({
callId: callDetails.callId,
enabled: !hasLocalAudio,
});
};
2020-09-11 17:46:52 -07:00
toggleVideo = (): void => {
2020-06-04 11:16:19 -07:00
const { callDetails, hasLocalVideo, setLocalVideo } = this.props;
if (!callDetails) {
return;
}
setLocalVideo({ callId: callDetails.callId, enabled: !hasLocalVideo });
};
2020-09-11 17:46:52 -07:00
public render(): JSX.Element | null {
2020-06-04 11:16:19 -07:00
const {
callDetails,
callState,
hangUp,
hasLocalAudio,
hasLocalVideo,
hasRemoteVideo,
2020-08-26 20:03:42 -04:00
i18n,
2020-09-30 20:43:05 -04:00
togglePip,
2020-08-26 20:03:42 -04:00
toggleSettings,
2020-06-04 11:16:19 -07:00
} = this.props;
const { showControls } = this.state;
const isAudioOnly = !hasLocalVideo && !hasRemoteVideo;
if (!callDetails || !callState) {
return null;
}
const controlsFadeClass = classNames({
'module-ongoing-call__controls--fadeIn':
(showControls || isAudioOnly) && callState !== CallState.Accepted,
'module-ongoing-call__controls--fadeOut':
!showControls && !isAudioOnly && callState === CallState.Accepted,
});
2020-10-07 21:25:33 -04:00
const videoButtonType = hasLocalVideo
? CallingButtonType.VIDEO_ON
: CallingButtonType.VIDEO_OFF;
const audioButtonType = hasLocalAudio
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
2020-06-04 11:16:19 -07:00
return (
<div
2020-10-07 21:25:33 -04:00
className="module-calling__container"
2020-06-04 11:16:19 -07:00
onMouseMove={this.showControls}
role="group"
>
<div
className={classNames(
2020-10-07 21:25:33 -04:00
'module-calling__header',
2020-06-04 11:16:19 -07:00
'module-ongoing-call__header',
controlsFadeClass
)}
>
2020-10-07 21:25:33 -04:00
<div className="module-calling__header--header-name">
{callDetails.title}
2020-06-04 11:16:19 -07:00
</div>
{this.renderMessage(callState)}
2020-10-07 21:25:33 -04:00
<div className="module-calling-tools">
2020-08-26 20:03:42 -04:00
<button
2020-09-11 17:46:52 -07:00
type="button"
2020-08-26 20:03:42 -04:00
aria-label={i18n('callingDeviceSelection__settings')}
2020-10-07 21:25:33 -04:00
className="module-calling-tools__button module-calling-button__settings"
2020-08-26 20:03:42 -04:00
onClick={toggleSettings}
/>
2020-09-30 20:43:05 -04:00
<button
type="button"
aria-label={i18n('calling__pip')}
2020-10-07 21:25:33 -04:00
className="module-calling-tools__button module-calling-button__pip"
2020-09-30 20:43:05 -04:00
onClick={togglePip}
/>
</div>
2020-06-04 11:16:19 -07:00
</div>
{hasRemoteVideo
? this.renderRemoteVideo()
: this.renderAvatar(callDetails)}
{hasLocalVideo ? this.renderLocalVideo() : null}
<div
className={classNames(
'module-ongoing-call__actions',
controlsFadeClass
)}
>
<CallingButton
2020-10-07 21:25:33 -04:00
buttonType={videoButtonType}
i18n={i18n}
2020-06-04 11:16:19 -07:00
onClick={this.toggleVideo}
2020-10-07 21:25:33 -04:00
tooltipDistance={24}
2020-06-04 11:16:19 -07:00
/>
<CallingButton
2020-10-07 21:25:33 -04:00
buttonType={audioButtonType}
i18n={i18n}
2020-06-04 11:16:19 -07:00
onClick={this.toggleAudio}
2020-10-07 21:25:33 -04:00
tooltipDistance={24}
2020-06-04 11:16:19 -07:00
/>
<CallingButton
2020-10-07 21:25:33 -04:00
buttonType={CallingButtonType.HANG_UP}
i18n={i18n}
2020-06-04 11:16:19 -07:00
onClick={() => {
hangUp({ callId: callDetails.callId });
}}
2020-10-07 21:25:33 -04:00
tooltipDistance={24}
2020-06-04 11:16:19 -07:00
/>
</div>
</div>
);
}
private renderAvatar(callDetails: CallDetailsType) {
const { i18n } = this.props;
const {
avatarPath,
2020-07-23 18:35:32 -07:00
color,
2020-06-04 11:16:19 -07:00
name,
phoneNumber,
profileName,
2020-07-23 18:35:32 -07:00
title,
2020-06-04 11:16:19 -07:00
} = callDetails;
return (
<div className="module-ongoing-call__remote-video-disabled">
<Avatar
avatarPath={avatarPath}
2020-07-23 18:35:32 -07:00
color={color || 'ultramarine'}
2020-06-04 11:16:19 -07:00
noteToSelf={false}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
2020-07-23 18:35:32 -07:00
title={title}
2020-06-04 11:16:19 -07:00
size={112}
/>
</div>
);
}
private renderLocalVideo() {
return (
<video
className="module-ongoing-call__local-video"
ref={this.localVideoRef}
autoPlay
/>
);
}
private renderRemoteVideo() {
return (
<canvas
className="module-ongoing-call__remote-video-enabled"
ref={this.remoteVideoRef}
/>
);
}
private renderMessage(callState: CallState) {
const { i18n } = this.props;
2020-09-11 17:46:52 -07:00
const { acceptedDuration } = this.state;
2020-06-04 11:16:19 -07:00
let message = null;
if (callState === CallState.Prering) {
message = i18n('outgoingCallPrering');
} else if (callState === CallState.Ringing) {
message = i18n('outgoingCallRinging');
} else if (callState === CallState.Reconnecting) {
message = i18n('callReconnecting');
2020-09-11 17:46:52 -07:00
} else if (callState === CallState.Accepted && acceptedDuration) {
message = i18n('callDuration', [this.renderDuration(acceptedDuration)]);
2020-06-04 11:16:19 -07:00
}
if (!message) {
return null;
}
return <div className="module-ongoing-call__header-message">{message}</div>;
}
2020-09-11 17:46:52 -07:00
// eslint-disable-next-line class-methods-use-this
2020-06-04 11:16:19 -07:00
private renderDuration(ms: number): string {
const secs = Math.floor((ms / 1000) % 60)
.toString()
.padStart(2, '0');
const mins = Math.floor((ms / 60000) % 60)
.toString()
.padStart(2, '0');
const hours = Math.floor(ms / 3600000);
if (hours > 0) {
return `${hours}:${mins}:${secs}`;
}
return `${mins}:${secs}`;
}
}