From 8073ccd32c3b354f5f9caeb97b179b1c90c9231f Mon Sep 17 00:00:00 2001 From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com> Date: Mon, 26 Oct 2020 17:13:42 -0500 Subject: [PATCH] Rewrite component with hooks --- ts/components/CallScreen.tsx | 541 ++++++++++++++++------------------- ts/util/lint/exceptions.json | 35 +-- 2 files changed, 251 insertions(+), 325 deletions(-) diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 189e2b6994..b34eeafbaa 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -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 { CallDetailsType, @@ -29,329 +30,269 @@ export type PropsType = { toggleSettings: () => void; }; -type StateType = { - acceptedDuration: number | null; - showControls: boolean; -}; +export const CallScreen: React.FC = ({ + callDetails, + callState, + hangUp, + hasLocalAudio, + hasLocalVideo, + hasRemoteVideo, + i18n, + setLocalAudio, + setLocalVideo, + setLocalPreview, + setRendererCanvas, + togglePip, + toggleSettings, +}) => { + const { acceptedTime, callId } = callDetails || {}; -export class CallScreen extends React.Component { - private interval: NodeJS.Timeout | null; - - private controlsFadeTimer: NodeJS.Timeout | null; - - private readonly localVideoRef: React.RefObject; - - private readonly remoteVideoRef: React.RefObject; - - 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) { + const toggleAudio = useCallback(() => { + if (!callId) { return; } setLocalAudio({ - callId: callDetails.callId, + callId, enabled: !hasLocalAudio, }); - }; + }, [callId, setLocalAudio, hasLocalAudio]); - toggleVideo = (): void => { - const { callDetails, hasLocalVideo, setLocalVideo } = this.props; - - if (!callDetails) { + const toggleVideo = useCallback(() => { + if (!callId) { return; } - setLocalVideo({ callId: callDetails.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, + setLocalVideo({ + callId, + enabled: !hasLocalVideo, }); + }, [callId, setLocalVideo, hasLocalVideo]); - const videoButtonType = hasLocalVideo - ? CallingButtonType.VIDEO_ON - : CallingButtonType.VIDEO_OFF; - const audioButtonType = hasLocalAudio - ? CallingButtonType.AUDIO_ON - : CallingButtonType.AUDIO_OFF; + const [acceptedDuration, setAcceptedDuration] = useState(null); + const [showControls, setShowControls] = useState(true); - return ( + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(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 ( +
{ + setShowControls(true); + }} + role="group" + >
-
-
- {callDetails.title} -
- {this.renderMessage(callState)} -
-
+
+ {callDetails.title}
- {hasRemoteVideo - ? this.renderRemoteVideo() - : this.renderAvatar(callDetails)} - {hasLocalVideo ? this.renderLocalVideo() : null} -
- +
- ); - } - - private renderAvatar(callDetails: CallDetailsType) { - const { i18n } = this.props; - const { - avatarPath, - color, - name, - phoneNumber, - profileName, - title, - } = callDetails; - return ( -
- + ) : ( + renderAvatar(i18n, callDetails) + )} + {hasLocalVideo && ( +
+ ); +}; - private renderLocalVideo() { - return ( -
+ ); +} + +function renderHeaderMessage( + i18n: LocalizerType, + callState: CallState, + acceptedDuration: null | number +): JSX.Element | null { + 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'); + } else if (callState === CallState.Accepted && acceptedDuration) { + message = i18n('callDuration', [renderDuration(acceptedDuration)]); + } + + if (!message) { + return null; + } + return
{message}
; +} + +function 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}`; } diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index fb6a10c36d..aa0b56305b 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -14388,37 +14388,22 @@ "reasonDetail": "Doesn't touch the DOM." }, { - "rule": "React-createRef", + "rule": "React-useRef", "path": "ts/components/CallScreen.js", - "line": " this.localVideoRef = react_1.default.createRef();", - "lineNumber": 87, + "line": " const localVideoRef = react_1.useRef(null);", + "lineNumber": 41, "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", - "line": " this.remoteVideoRef = react_1.default.createRef();", - "lineNumber": 88, + "line": " const remoteVideoRef = react_1.useRef(null);", + "lineNumber": 42, "reasonCategory": "usageTrusted", - "updated": "2020-09-14T23:03:44.863Z" - }, - { - "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" + "updated": "2020-10-26T21:35:52.858Z", + "reasonDetail": "Used to get the remote video element for rendering." }, { "rule": "React-useRef",