From 3468de255d01da44ecf65fe6f9a34ebff87e3552 Mon Sep 17 00:00:00 2001 From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com> Date: Fri, 6 Nov 2020 11:36:37 -0600 Subject: [PATCH] Redux state: Allow multiple calls to be stored --- preload.js | 1 + ts/components/CallManager.stories.tsx | 72 ++- ts/components/CallManager.tsx | 194 +++++--- ts/components/CallNeedPermissionScreen.tsx | 27 +- ts/components/CallScreen.stories.tsx | 26 +- ts/components/CallScreen.tsx | 63 ++- ts/components/CallingLobby.stories.tsx | 12 +- ts/components/CallingLobby.tsx | 28 +- ts/components/CallingPip.stories.tsx | 16 +- ts/components/CallingPip.tsx | 35 +- ts/components/IncomingCallBar.stories.tsx | 30 +- ts/components/IncomingCallBar.tsx | 50 ++- ts/services/calling.ts | 110 +++-- ts/state/ducks/calling.ts | 393 +++++++++++----- ts/state/ducks/noop.ts | 7 + ts/state/selectors/calling.ts | 33 ++ ts/state/smart/CallManager.tsx | 61 ++- ts/state/smart/ConversationHeader.tsx | 2 +- ts/test-electron/state/ducks/calling_test.ts | 420 +++++++++++++----- .../state/selectors/calling_test.ts | 106 +++++ ts/util/lint/exceptions.json | 20 +- 21 files changed, 1191 insertions(+), 515 deletions(-) create mode 100644 ts/state/selectors/calling.ts create mode 100644 ts/test-electron/state/selectors/calling_test.ts diff --git a/preload.js b/preload.js index 9975439e521f..4dcf1b8c46fc 100644 --- a/preload.js +++ b/preload.js @@ -565,6 +565,7 @@ try { require('./ts/test-electron/models/messages_test'); require('./ts/test-electron/linkPreviews/linkPreviewFetch_test'); require('./ts/test-electron/state/ducks/calling_test'); + require('./ts/test-electron/state/selectors/calling_test'); delete window.describe; diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index 055125b42a0b..ec0348e98561 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -13,11 +13,7 @@ import enMessages from '../../_locales/en/messages.json'; const i18n = setupI18n('en', enMessages); -const callDetails = { - callId: 0, - isIncoming: true, - isVideoCall: true, - +const conversation = { id: '3051234567', avatarPath: undefined, color: 'ultramarine' as ColorType, @@ -30,27 +26,20 @@ const callDetails = { const defaultProps = { availableCameras: [], acceptCall: action('accept-call'), - callDetails, - callState: CallState.Accepted, cancelCall: action('cancel-call'), closeNeedPermissionScreen: action('close-need-permission-screen'), declineCall: action('decline-call'), hangUp: action('hang-up'), - hasLocalAudio: true, - hasLocalVideo: true, - hasRemoteVideo: true, i18n, me: { color: 'ultramarine' as ColorType, title: 'Morty Smith', }, - pip: false, renderDeviceSelection: () =>
, setLocalAudio: action('set-local-audio'), setLocalPreview: action('set-local-preview'), setLocalVideo: action('set-local-video'), setRendererCanvas: action('set-renderer-canvas'), - settingsDialogOpen: false, startCall: action('start-call'), toggleParticipants: action('toggle-participants'), togglePip: action('toggle-pip'), @@ -59,20 +48,71 @@ const defaultProps = { const permutations = [ { - title: 'Call Manager (ongoing)', + title: 'Call Manager (no call)', props: {}, }, + { + title: 'Call Manager (ongoing)', + props: { + activeCall: { + call: { + conversationId: '3051234567', + callState: CallState.Accepted, + isIncoming: false, + isVideoCall: true, + hasRemoteVideo: true, + }, + activeCallState: { + conversationId: '3051234567', + joinedAt: Date.now(), + hasLocalAudio: true, + hasLocalVideo: false, + participantsList: false, + pip: false, + settingsDialogOpen: false, + }, + conversation, + }, + }, + }, { title: 'Call Manager (ringing)', props: { - callState: CallState.Ringing, + incomingCall: { + call: { + conversationId: '3051234567', + callState: CallState.Ringing, + isIncoming: true, + isVideoCall: true, + hasRemoteVideo: true, + }, + conversation, + }, }, }, { title: 'Call Manager (call request needed)', props: { - callState: CallState.Ended, - callEndedReason: CallEndedReason.RemoteHangupNeedPermission, + activeCall: { + call: { + conversationId: '3051234567', + callState: CallState.Ended, + callEndedReason: CallEndedReason.RemoteHangupNeedPermission, + isIncoming: false, + isVideoCall: true, + hasRemoteVideo: true, + }, + activeCallState: { + conversationId: '3051234567', + joinedAt: Date.now(), + hasLocalAudio: true, + hasLocalVideo: false, + participantsList: false, + pip: false, + settingsDialogOpen: false, + }, + conversation, + }, }, }, ]; diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx index 983789ee4757..ba317ef872d6 100644 --- a/ts/components/CallManager.tsx +++ b/ts/components/CallManager.tsx @@ -5,112 +5,156 @@ import React from 'react'; import { CallingPip } from './CallingPip'; import { CallNeedPermissionScreen } from './CallNeedPermissionScreen'; import { CallingLobby } from './CallingLobby'; -import { CallScreen, PropsType as CallScreenPropsType } from './CallScreen'; -import { - IncomingCallBar, - PropsType as IncomingCallBarPropsType, -} from './IncomingCallBar'; +import { CallScreen } from './CallScreen'; +import { IncomingCallBar } from './IncomingCallBar'; import { CallState, CallEndedReason } from '../types/Calling'; -import { CallDetailsType, OutgoingCallType } from '../state/ducks/calling'; +import { + ActiveCallStateType, + AcceptCallType, + DeclineCallType, + DirectCallStateType, + StartCallType, + SetLocalAudioType, + HangUpType, + SetLocalPreviewType, + SetLocalVideoType, + SetRendererCanvasType, +} from '../state/ducks/calling'; +import { LocalizerType } from '../types/Util'; +import { ColorType } from '../types/Colors'; -type CallManagerPropsType = { +interface PropsType { + activeCall?: { + call: DirectCallStateType; + activeCallState: ActiveCallStateType; + conversation: { + id: string; + avatarPath?: string; + color?: ColorType; + title: string; + name?: string; + phoneNumber?: string; + profileName?: string; + }; + }; availableCameras: Array; - callDetails?: CallDetailsType; - callEndedReason?: CallEndedReason; - callState?: CallState; cancelCall: () => void; - pip: boolean; closeNeedPermissionScreen: () => void; + incomingCall?: { + call: DirectCallStateType; + conversation: { + id: string; + avatarPath?: string; + color?: ColorType; + title: string; + name?: string; + phoneNumber?: string; + profileName?: string; + }; + }; renderDeviceSelection: () => JSX.Element; - settingsDialogOpen: boolean; - startCall: (payload: OutgoingCallType) => void; + startCall: (payload: StartCallType) => void; toggleParticipants: () => void; -}; - -type PropsType = IncomingCallBarPropsType & - CallScreenPropsType & - CallManagerPropsType; + acceptCall: (_: AcceptCallType) => void; + declineCall: (_: DeclineCallType) => void; + i18n: LocalizerType; + me: { + avatarPath?: string; + color?: ColorType; + name?: string; + phoneNumber?: string; + profileName?: string; + title: string; + }; + setLocalAudio: (_: SetLocalAudioType) => void; + setLocalVideo: (_: SetLocalVideoType) => void; + setLocalPreview: (_: SetLocalPreviewType) => void; + setRendererCanvas: (_: SetRendererCanvasType) => void; + hangUp: (_: HangUpType) => void; + togglePip: () => void; + toggleSettings: () => void; +} export const CallManager = ({ acceptCall, + activeCall, availableCameras, - callDetails, - callState, - callEndedReason, cancelCall, closeNeedPermissionScreen, declineCall, hangUp, - hasLocalAudio, - hasLocalVideo, - hasRemoteVideo, i18n, + incomingCall, me, - pip, renderDeviceSelection, setLocalAudio, setLocalPreview, setLocalVideo, setRendererCanvas, - settingsDialogOpen, startCall, toggleParticipants, togglePip, toggleSettings, }: PropsType): JSX.Element | null => { - if (!callDetails) { - return null; - } - const incoming = callDetails.isIncoming; - const outgoing = !incoming; - const ongoing = - callState === CallState.Accepted || callState === CallState.Reconnecting; - const ringing = callState === CallState.Ringing; - const ended = callState === CallState.Ended; + if (activeCall) { + const { call, activeCallState, conversation } = activeCall; + const { callState, callEndedReason } = call; + const { + joinedAt, + hasLocalAudio, + hasLocalVideo, + settingsDialogOpen, + pip, + } = activeCallState; - if (ended) { - if (callEndedReason === CallEndedReason.RemoteHangupNeedPermission) { + const ended = callState === CallState.Ended; + if (ended) { + if (callEndedReason === CallEndedReason.RemoteHangupNeedPermission) { + return ( + + ); + } + } + + if (!callState) { return ( - + <> + { + startCall({ + conversationId: conversation.id, + hasLocalAudio, + hasLocalVideo, + }); + }} + setLocalPreview={setLocalPreview} + setLocalAudio={setLocalAudio} + setLocalVideo={setLocalVideo} + toggleParticipants={toggleParticipants} + toggleSettings={toggleSettings} + /> + {settingsDialogOpen && renderDeviceSelection()} + ); } - return null; - } - if (!callState) { - return ( - <> - { - startCall({ callDetails }); - }} - setLocalPreview={setLocalPreview} - setLocalAudio={setLocalAudio} - setLocalVideo={setLocalVideo} - toggleParticipants={toggleParticipants} - toggleSettings={toggleSettings} - /> - {settingsDialogOpen && renderDeviceSelection()} - - ); - } + const hasRemoteVideo = Boolean(call.hasRemoteVideo); - if (outgoing || ongoing) { if (pip) { return ( ); } - // Incoming && Prering return null; }; diff --git a/ts/components/CallNeedPermissionScreen.tsx b/ts/components/CallNeedPermissionScreen.tsx index 36b8c067bf38..04dca0f9181b 100644 --- a/ts/components/CallNeedPermissionScreen.tsx +++ b/ts/components/CallNeedPermissionScreen.tsx @@ -2,14 +2,21 @@ // SPDX-License-Identifier: AGPL-3.0-only import React, { useRef, useEffect } from 'react'; -import { CallDetailsType } from '../state/ducks/calling'; import { LocalizerType } from '../types/Util'; import { Avatar } from './Avatar'; import { Intl } from './Intl'; import { ContactName } from './conversation/ContactName'; +import { ColorType } from '../types/Colors'; interface Props { - callDetails: CallDetailsType; + conversation: { + avatarPath?: string; + color?: ColorType; + name?: string; + phoneNumber?: string; + profileName?: string; + title: string; + }; i18n: LocalizerType; close: () => void; } @@ -17,11 +24,11 @@ interface Props { const AUTO_CLOSE_MS = 10000; export const CallNeedPermissionScreen: React.FC = ({ - callDetails, + conversation, i18n, close, }) => { - const title = callDetails.title || i18n('unknownContact'); + const title = conversation.title || i18n('unknownContact'); const autoCloseAtRef = useRef(Date.now() + AUTO_CLOSE_MS); useEffect(() => { @@ -32,15 +39,15 @@ export const CallNeedPermissionScreen: React.FC = ({ return (
diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx index 769586c0130d..475177c41535 100644 --- a/ts/components/CallScreen.stories.tsx +++ b/ts/components/CallScreen.stories.tsx @@ -14,23 +14,16 @@ import enMessages from '../../_locales/en/messages.json'; const i18n = setupI18n('en', enMessages); -const callDetails = { - acceptedTime: Date.now(), - callId: 0, - isIncoming: true, - isVideoCall: true, - - id: '3051234567', - avatarPath: undefined, - color: 'ultramarine' as ColorType, - title: 'Rick Sanchez', - name: 'Rick Sanchez', - phoneNumber: '3051234567', - profileName: 'Rick Sanchez', -}; - const createProps = (overrideProps: Partial = {}): PropsType => ({ - callDetails, + conversation: { + id: '3051234567', + avatarPath: undefined, + color: 'ultramarine' as ColorType, + title: 'Rick Sanchez', + name: 'Rick Sanchez', + phoneNumber: '3051234567', + profileName: 'Rick Sanchez', + }, callState: select( 'callState', CallState, @@ -44,6 +37,7 @@ const createProps = (overrideProps: Partial = {}): PropsType => ({ overrideProps.hasRemoteVideo || false ), i18n, + joinedAt: Date.now(), me: { color: 'ultramarine' as ColorType, name: 'Morty Smith', diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index d2e5241f3f1b..66e011ad66b4 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -5,7 +5,6 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { noop } from 'lodash'; import classNames from 'classnames'; import { - CallDetailsType, HangUpType, SetLocalAudioType, SetLocalPreviewType, @@ -20,13 +19,22 @@ import { ColorType } from '../types/Colors'; import { LocalizerType } from '../types/Util'; export type PropsType = { - callDetails?: CallDetailsType; - callState?: CallState; + conversation: { + id: string; + avatarPath?: string; + color?: ColorType; + title: string; + name?: string; + phoneNumber?: string; + profileName?: string; + }; + callState: CallState; hangUp: (_: HangUpType) => void; hasLocalAudio: boolean; hasLocalVideo: boolean; hasRemoteVideo: boolean; i18n: LocalizerType; + joinedAt?: number; me: { avatarPath?: string; color?: ColorType; @@ -44,13 +52,14 @@ export type PropsType = { }; export const CallScreen: React.FC = ({ - callDetails, callState, + conversation, hangUp, hasLocalAudio, hasLocalVideo, hasRemoteVideo, i18n, + joinedAt, me, setLocalAudio, setLocalVideo, @@ -59,29 +68,17 @@ export const CallScreen: React.FC = ({ togglePip, toggleSettings, }) => { - const { acceptedTime, callId } = callDetails || {}; - const toggleAudio = useCallback(() => { - if (!callId) { - return; - } - setLocalAudio({ - callId, enabled: !hasLocalAudio, }); - }, [callId, setLocalAudio, hasLocalAudio]); + }, [setLocalAudio, hasLocalAudio]); const toggleVideo = useCallback(() => { - if (!callId) { - return; - } - setLocalVideo({ - callId, enabled: !hasLocalVideo, }); - }, [callId, setLocalVideo, hasLocalVideo]); + }, [setLocalVideo, hasLocalVideo]); const [acceptedDuration, setAcceptedDuration] = useState(null); const [showControls, setShowControls] = useState(true); @@ -100,15 +97,15 @@ export const CallScreen: React.FC = ({ }, [setLocalPreview, setRendererCanvas]); useEffect(() => { - if (!acceptedTime) { + if (!joinedAt) { return noop; } // It's really jumpy with a value of 500ms. const interval = setInterval(() => { - setAcceptedDuration(Date.now() - acceptedTime); + setAcceptedDuration(Date.now() - joinedAt); }, 100); return clearInterval.bind(null, interval); - }, [acceptedTime]); + }, [joinedAt]); useEffect(() => { if (!showControls) { @@ -147,10 +144,6 @@ export const CallScreen: React.FC = ({ const isAudioOnly = !hasLocalVideo && !hasRemoteVideo; - if (!callDetails || !callState) { - return null; - } - const controlsFadeClass = classNames({ 'module-ongoing-call__controls--fadeIn': (showControls || isAudioOnly) && callState !== CallState.Accepted, @@ -181,7 +174,7 @@ export const CallScreen: React.FC = ({ )} >
- {callDetails.title} + {conversation.title}
{renderHeaderMessage(i18n, callState, acceptedDuration)}
@@ -205,7 +198,7 @@ export const CallScreen: React.FC = ({ ref={remoteVideoRef} /> ) : ( - renderAvatar(i18n, callDetails) + renderAvatar(i18n, conversation) )}
{/* This layout-only element is not ideal. @@ -233,7 +226,7 @@ export const CallScreen: React.FC = ({ buttonType={CallingButtonType.HANG_UP} i18n={i18n} onClick={() => { - hangUp({ callId }); + hangUp({ conversationId: conversation.id }); }} tooltipDistance={24} /> @@ -269,16 +262,22 @@ export const CallScreen: React.FC = ({ function renderAvatar( i18n: LocalizerType, - callDetails: CallDetailsType -): JSX.Element { - const { + { avatarPath, color, name, phoneNumber, profileName, title, - } = callDetails; + }: { + avatarPath?: string; + color?: ColorType; + title: string; + name?: string; + phoneNumber?: string; + profileName?: string; + } +): JSX.Element { return (
= {}): PropsType => ({ availableCameras: overrideProps.availableCameras || [camera], - callDetails, + conversation, hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false), hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false), i18n, @@ -60,8 +56,8 @@ story.add('Default', () => { return ( diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index f6dd5f598973..09108ead0197 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { - CallDetailsType, SetLocalAudioType, SetLocalPreviewType, SetLocalVideoType, @@ -15,10 +14,15 @@ import { } from './CallingButton'; import { CallBackgroundBlur } from './CallBackgroundBlur'; import { LocalizerType } from '../types/Util'; +import { ColorType } from '../types/Colors'; export type PropsType = { availableCameras: Array; - callDetails: CallDetailsType; + conversation: { + avatarPath?: string; + color?: ColorType; + title: string; + }; hasLocalAudio: boolean; hasLocalVideo: boolean; i18n: LocalizerType; @@ -34,7 +38,7 @@ export type PropsType = { export const CallingLobby = ({ availableCameras, - callDetails, + conversation, hasLocalAudio, hasLocalVideo, i18n, @@ -50,20 +54,12 @@ export const CallingLobby = ({ const localVideoRef = React.useRef(null); const toggleAudio = React.useCallback((): void => { - if (!callDetails) { - return; - } - setLocalAudio({ enabled: !hasLocalAudio }); - }, [callDetails, hasLocalAudio, setLocalAudio]); + }, [hasLocalAudio, setLocalAudio]); const toggleVideo = React.useCallback((): void => { - if (!callDetails) { - return; - } - setLocalVideo({ enabled: !hasLocalVideo }); - }, [callDetails, hasLocalVideo, setLocalVideo]); + }, [hasLocalVideo, setLocalVideo]); React.useEffect(() => { setLocalPreview({ element: localVideoRef }); @@ -112,7 +108,7 @@ export const CallingLobby = ({
- {callDetails.title} + {conversation.title}
{isGroupCall ? ( @@ -136,8 +132,8 @@ export const CallingLobby = ({