Rewrite <CallScreen> component with hooks

This commit is contained in:
Evan Hahn 2020-10-26 17:13:42 -05:00 committed by Evan Hahn
parent 05a91a100f
commit 8073ccd32c
2 changed files with 251 additions and 325 deletions

View file

@ -1,4 +1,5 @@
import React from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
import { noop } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
CallDetailsType, CallDetailsType,
@ -29,329 +30,269 @@ export type PropsType = {
toggleSettings: () => void; toggleSettings: () => void;
}; };
type StateType = { export const CallScreen: React.FC<PropsType> = ({
acceptedDuration: number | null; callDetails,
showControls: boolean; callState,
}; hangUp,
hasLocalAudio,
hasLocalVideo,
hasRemoteVideo,
i18n,
setLocalAudio,
setLocalVideo,
setLocalPreview,
setRendererCanvas,
togglePip,
toggleSettings,
}) => {
const { acceptedTime, callId } = callDetails || {};
export class CallScreen extends React.Component<PropsType, StateType> { const toggleAudio = useCallback(() => {
private interval: NodeJS.Timeout | null; if (!callId) {
private controlsFadeTimer: NodeJS.Timeout | null;
private readonly localVideoRef: React.RefObject<HTMLVideoElement>;
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();
}
public componentDidMount(): void {
const { setLocalPreview, setRendererCanvas } = this.props;
// It's really jump with a value of 500ms.
this.interval = setInterval(this.updateAcceptedTimer, 100);
this.fadeControls();
document.addEventListener('keydown', this.handleKeyDown);
setLocalPreview({ element: this.localVideoRef });
setRendererCanvas({ element: this.remoteVideoRef });
}
public componentWillUnmount(): void {
const { setLocalPreview, setRendererCanvas } = this.props;
document.removeEventListener('keydown', this.handleKeyDown);
if (this.interval) {
clearInterval(this.interval);
}
if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer);
}
setLocalPreview({ element: undefined });
setRendererCanvas({ element: undefined });
}
updateAcceptedTimer = (): void => {
const { callDetails } = this.props;
if (!callDetails) {
return;
}
if (callDetails.acceptedTime) {
this.setState({
acceptedDuration: Date.now() - callDetails.acceptedTime,
});
}
};
handleKeyDown = (event: KeyboardEvent): void => {
const { callDetails } = this.props;
if (!callDetails) {
return;
}
let eventHandled = false;
if (event.shiftKey && (event.key === 'V' || event.key === 'v')) {
this.toggleVideo();
eventHandled = true;
} else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) {
this.toggleAudio();
eventHandled = true;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
this.showControls();
}
};
showControls = (): void => {
const { showControls } = this.state;
if (!showControls) {
this.setState({
showControls: true,
});
}
this.fadeControls();
};
fadeControls = (): void => {
if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer);
}
this.controlsFadeTimer = setTimeout(() => {
this.setState({
showControls: false,
});
}, 5000);
};
toggleAudio = (): void => {
const { callDetails, hasLocalAudio, setLocalAudio } = this.props;
if (!callDetails) {
return; return;
} }
setLocalAudio({ setLocalAudio({
callId: callDetails.callId, callId,
enabled: !hasLocalAudio, enabled: !hasLocalAudio,
}); });
}; }, [callId, setLocalAudio, hasLocalAudio]);
toggleVideo = (): void => { const toggleVideo = useCallback(() => {
const { callDetails, hasLocalVideo, setLocalVideo } = this.props; if (!callId) {
if (!callDetails) {
return; return;
} }
setLocalVideo({ callId: callDetails.callId, enabled: !hasLocalVideo }); setLocalVideo({
}; callId,
enabled: !hasLocalVideo,
public render(): JSX.Element | null {
const {
callDetails,
callState,
hangUp,
hasLocalAudio,
hasLocalVideo,
hasRemoteVideo,
i18n,
togglePip,
toggleSettings,
} = 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,
}); });
}, [callId, setLocalVideo, hasLocalVideo]);
const videoButtonType = hasLocalVideo const [acceptedDuration, setAcceptedDuration] = useState<number | null>(null);
? CallingButtonType.VIDEO_ON const [showControls, setShowControls] = useState(true);
: CallingButtonType.VIDEO_OFF;
const audioButtonType = hasLocalAudio
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
return ( const localVideoRef = useRef<HTMLVideoElement | null>(null);
const remoteVideoRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
setLocalPreview({ element: localVideoRef });
setRendererCanvas({ element: remoteVideoRef });
return () => {
setLocalPreview({ element: undefined });
setRendererCanvas({ element: undefined });
};
}, [setLocalPreview, setRendererCanvas]);
useEffect(() => {
if (!acceptedTime) {
return noop;
}
// It's really jumpy with a value of 500ms.
const interval = setInterval(() => {
setAcceptedDuration(Date.now() - acceptedTime);
}, 100);
return clearInterval.bind(null, interval);
}, [acceptedTime]);
useEffect(() => {
if (!showControls) {
return noop;
}
const timer = setTimeout(() => {
setShowControls(false);
}, 5000);
return clearInterval.bind(null, timer);
}, [showControls]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent): void => {
let eventHandled = false;
if (event.shiftKey && (event.key === 'V' || event.key === 'v')) {
toggleVideo();
eventHandled = true;
} else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) {
toggleAudio();
eventHandled = true;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
setShowControls(true);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [toggleAudio, toggleVideo]);
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,
});
const videoButtonType = hasLocalVideo
? CallingButtonType.VIDEO_ON
: CallingButtonType.VIDEO_OFF;
const audioButtonType = hasLocalAudio
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
return (
<div
className="module-calling__container"
onMouseMove={() => {
setShowControls(true);
}}
role="group"
>
<div <div
className="module-calling__container" className={classNames(
onMouseMove={this.showControls} 'module-calling__header',
role="group" 'module-ongoing-call__header',
controlsFadeClass
)}
> >
<div <div className="module-calling__header--header-name">
className={classNames( {callDetails.title}
'module-calling__header',
'module-ongoing-call__header',
controlsFadeClass
)}
>
<div className="module-calling__header--header-name">
{callDetails.title}
</div>
{this.renderMessage(callState)}
<div className="module-calling-tools">
<button
type="button"
aria-label={i18n('callingDeviceSelection__settings')}
className="module-calling-tools__button module-calling-button__settings"
onClick={toggleSettings}
/>
<button
type="button"
aria-label={i18n('calling__pip')}
className="module-calling-tools__button module-calling-button__pip"
onClick={togglePip}
/>
</div>
</div> </div>
{hasRemoteVideo {renderHeaderMessage(i18n, callState, acceptedDuration)}
? this.renderRemoteVideo() <div className="module-calling-tools">
: this.renderAvatar(callDetails)} <button
{hasLocalVideo ? this.renderLocalVideo() : null} type="button"
<div aria-label={i18n('callingDeviceSelection__settings')}
className={classNames( className="module-calling-tools__button module-calling-button__settings"
'module-ongoing-call__actions', onClick={toggleSettings}
controlsFadeClass
)}
>
<CallingButton
buttonType={videoButtonType}
i18n={i18n}
onClick={this.toggleVideo}
tooltipDistance={24}
/> />
<CallingButton <button
buttonType={audioButtonType} type="button"
i18n={i18n} aria-label={i18n('calling__pip')}
onClick={this.toggleAudio} className="module-calling-tools__button module-calling-button__pip"
tooltipDistance={24} onClick={togglePip}
/>
<CallingButton
buttonType={CallingButtonType.HANG_UP}
i18n={i18n}
onClick={() => {
hangUp({ callId: callDetails.callId });
}}
tooltipDistance={24}
/> />
</div> </div>
</div> </div>
); {hasRemoteVideo ? (
} <canvas
className="module-ongoing-call__remote-video-enabled"
private renderAvatar(callDetails: CallDetailsType) { ref={remoteVideoRef}
const { i18n } = this.props; />
const { ) : (
avatarPath, renderAvatar(i18n, callDetails)
color, )}
name, {hasLocalVideo && (
phoneNumber, <video
profileName, className="module-ongoing-call__local-video"
title, ref={localVideoRef}
} = callDetails; autoPlay
return ( />
<div className="module-ongoing-call__remote-video-disabled"> )}
<Avatar <div
avatarPath={avatarPath} className={classNames(
color={color || 'ultramarine'} 'module-ongoing-call__actions',
noteToSelf={false} controlsFadeClass
conversationType="direct" )}
>
<CallingButton
buttonType={videoButtonType}
i18n={i18n} i18n={i18n}
name={name} onClick={toggleVideo}
phoneNumber={phoneNumber} tooltipDistance={24}
profileName={profileName} />
title={title} <CallingButton
size={112} buttonType={audioButtonType}
i18n={i18n}
onClick={toggleAudio}
tooltipDistance={24}
/>
<CallingButton
buttonType={CallingButtonType.HANG_UP}
i18n={i18n}
onClick={() => {
hangUp({ callId });
}}
tooltipDistance={24}
/> />
</div> </div>
); </div>
} );
};
private renderLocalVideo() { function renderAvatar(
return ( i18n: LocalizerType,
<video callDetails: CallDetailsType
className="module-ongoing-call__local-video" ): JSX.Element {
ref={this.localVideoRef} const {
autoPlay avatarPath,
color,
name,
phoneNumber,
profileName,
title,
} = callDetails;
return (
<div className="module-ongoing-call__remote-video-disabled">
<Avatar
avatarPath={avatarPath}
color={color || 'ultramarine'}
noteToSelf={false}
conversationType="direct"
i18n={i18n}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
size={112}
/> />
); </div>
} );
}
private renderRemoteVideo() {
return ( function renderHeaderMessage(
<canvas i18n: LocalizerType,
className="module-ongoing-call__remote-video-enabled" callState: CallState,
ref={this.remoteVideoRef} acceptedDuration: null | number
/> ): JSX.Element | null {
); let message = null;
} if (callState === CallState.Prering) {
message = i18n('outgoingCallPrering');
private renderMessage(callState: CallState) { } else if (callState === CallState.Ringing) {
const { i18n } = this.props; message = i18n('outgoingCallRinging');
const { acceptedDuration } = this.state; } else if (callState === CallState.Reconnecting) {
message = i18n('callReconnecting');
let message = null; } else if (callState === CallState.Accepted && acceptedDuration) {
if (callState === CallState.Prering) { message = i18n('callDuration', [renderDuration(acceptedDuration)]);
message = i18n('outgoingCallPrering'); }
} else if (callState === CallState.Ringing) {
message = i18n('outgoingCallRinging'); if (!message) {
} else if (callState === CallState.Reconnecting) { return null;
message = i18n('callReconnecting'); }
} else if (callState === CallState.Accepted && acceptedDuration) { return <div className="module-ongoing-call__header-message">{message}</div>;
message = i18n('callDuration', [ }
CallScreen.renderDuration(acceptedDuration),
]); function renderDuration(ms: number): string {
} const secs = Math.floor((ms / 1000) % 60)
.toString()
if (!message) { .padStart(2, '0');
return null; const mins = Math.floor((ms / 60000) % 60)
} .toString()
return <div className="module-ongoing-call__header-message">{message}</div>; .padStart(2, '0');
} const hours = Math.floor(ms / 3600000);
if (hours > 0) {
static renderDuration(ms: number): string { return `${hours}:${mins}:${secs}`;
const secs = Math.floor((ms / 1000) % 60) }
.toString() return `${mins}:${secs}`;
.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}`;
}
} }

View file

@ -14388,37 +14388,22 @@
"reasonDetail": "Doesn't touch the DOM." "reasonDetail": "Doesn't touch the DOM."
}, },
{ {
"rule": "React-createRef", "rule": "React-useRef",
"path": "ts/components/CallScreen.js", "path": "ts/components/CallScreen.js",
"line": " this.localVideoRef = react_1.default.createRef();", "line": " const localVideoRef = react_1.useRef(null);",
"lineNumber": 87, "lineNumber": 41,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-14T23:03:44.863Z" "updated": "2020-10-26T21:35:52.858Z",
"reasonDetail": "Used to get the local video element for rendering."
}, },
{ {
"rule": "React-createRef", "rule": "React-useRef",
"path": "ts/components/CallScreen.js", "path": "ts/components/CallScreen.js",
"line": " this.remoteVideoRef = react_1.default.createRef();", "line": " const remoteVideoRef = react_1.useRef(null);",
"lineNumber": 88, "lineNumber": 42,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-14T23:03:44.863Z" "updated": "2020-10-26T21:35:52.858Z",
}, "reasonDetail": "Used to get the remote video element for rendering."
{
"rule": "React-createRef",
"path": "ts/components/CallScreen.tsx",
"line": " this.localVideoRef = React.createRef();",
"lineNumber": 56,
"reasonCategory": "usageTrusted",
"updated": "2020-06-02T21:51:34.813Z",
"reasonDetail": "Used to render local preview video"
},
{
"rule": "React-createRef",
"path": "ts/components/CallScreen.tsx",
"line": " this.remoteVideoRef = React.createRef();",
"lineNumber": 57,
"reasonCategory": "usageTrusted",
"updated": "2020-09-14T23:03:44.863Z"
}, },
{ {
"rule": "React-useRef", "rule": "React-useRef",