Redux state: Allow multiple calls to be stored
This commit is contained in:
parent
753e0279c6
commit
3468de255d
21 changed files with 1191 additions and 515 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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: () => <div />,
|
||||
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: {
|
||||
incomingCall: {
|
||||
call: {
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
conversation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (call request needed)',
|
||||
props: {
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -5,80 +5,119 @@ 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 = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
callDetails?: CallDetailsType;
|
||||
callEndedReason?: CallEndedReason;
|
||||
callState?: CallState;
|
||||
cancelCall: () => void;
|
||||
pip: boolean;
|
||||
closeNeedPermissionScreen: () => void;
|
||||
renderDeviceSelection: () => JSX.Element;
|
||||
settingsDialogOpen: boolean;
|
||||
startCall: (payload: OutgoingCallType) => void;
|
||||
toggleParticipants: () => void;
|
||||
interface PropsType {
|
||||
activeCall?: {
|
||||
call: DirectCallStateType;
|
||||
activeCallState: ActiveCallStateType;
|
||||
conversation: {
|
||||
id: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
};
|
||||
|
||||
type PropsType = IncomingCallBarPropsType &
|
||||
CallScreenPropsType &
|
||||
CallManagerPropsType;
|
||||
};
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
cancelCall: () => void;
|
||||
closeNeedPermissionScreen: () => void;
|
||||
incomingCall?: {
|
||||
call: DirectCallStateType;
|
||||
conversation: {
|
||||
id: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
};
|
||||
};
|
||||
renderDeviceSelection: () => JSX.Element;
|
||||
startCall: (payload: StartCallType) => void;
|
||||
toggleParticipants: () => void;
|
||||
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;
|
||||
|
||||
const ended = callState === CallState.Ended;
|
||||
if (ended) {
|
||||
if (callEndedReason === CallEndedReason.RemoteHangupNeedPermission) {
|
||||
return (
|
||||
<CallNeedPermissionScreen
|
||||
close={closeNeedPermissionScreen}
|
||||
callDetails={callDetails}
|
||||
conversation={conversation}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!callState) {
|
||||
|
@ -86,14 +125,18 @@ export const CallManager = ({
|
|||
<>
|
||||
<CallingLobby
|
||||
availableCameras={availableCameras}
|
||||
callDetails={callDetails}
|
||||
conversation={conversation}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
isGroupCall={false}
|
||||
onCallCanceled={cancelCall}
|
||||
onJoinCall={() => {
|
||||
startCall({ callDetails });
|
||||
startCall({
|
||||
conversationId: conversation.id,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
});
|
||||
}}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setLocalAudio={setLocalAudio}
|
||||
|
@ -106,11 +149,12 @@ export const CallManager = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (outgoing || ongoing) {
|
||||
const hasRemoteVideo = Boolean(call.hasRemoteVideo);
|
||||
|
||||
if (pip) {
|
||||
return (
|
||||
<CallingPip
|
||||
callDetails={callDetails}
|
||||
conversation={conversation}
|
||||
hangUp={hangUp}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
|
@ -125,12 +169,13 @@ export const CallManager = ({
|
|||
return (
|
||||
<>
|
||||
<CallScreen
|
||||
callDetails={callDetails}
|
||||
conversation={conversation}
|
||||
callState={callState}
|
||||
hangUp={hangUp}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
joinedAt={joinedAt}
|
||||
me={me}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
setLocalPreview={setLocalPreview}
|
||||
|
@ -145,17 +190,18 @@ export const CallManager = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (incoming && ringing) {
|
||||
// In the future, we may want to show the incoming call bar when a call is active.
|
||||
if (incomingCall) {
|
||||
return (
|
||||
<IncomingCallBar
|
||||
acceptCall={acceptCall}
|
||||
callDetails={callDetails}
|
||||
declineCall={declineCall}
|
||||
i18n={i18n}
|
||||
call={incomingCall.call}
|
||||
conversation={incomingCall.conversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Incoming && Prering
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -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<Props> = ({
|
||||
callDetails,
|
||||
conversation,
|
||||
i18n,
|
||||
close,
|
||||
}) => {
|
||||
const title = callDetails.title || i18n('unknownContact');
|
||||
const title = conversation.title || i18n('unknownContact');
|
||||
|
||||
const autoCloseAtRef = useRef<number>(Date.now() + AUTO_CLOSE_MS);
|
||||
useEffect(() => {
|
||||
|
@ -32,15 +39,15 @@ export const CallNeedPermissionScreen: React.FC<Props> = ({
|
|||
return (
|
||||
<div className="module-call-need-permission-screen">
|
||||
<Avatar
|
||||
avatarPath={callDetails.avatarPath}
|
||||
color={callDetails.color || 'ultramarine'}
|
||||
avatarPath={conversation.avatarPath}
|
||||
color={conversation.color || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={callDetails.name}
|
||||
phoneNumber={callDetails.phoneNumber}
|
||||
profileName={callDetails.profileName}
|
||||
title={callDetails.title}
|
||||
name={conversation.name}
|
||||
phoneNumber={conversation.phoneNumber}
|
||||
profileName={conversation.profileName}
|
||||
title={conversation.title}
|
||||
size={112}
|
||||
/>
|
||||
|
||||
|
|
|
@ -14,12 +14,8 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const callDetails = {
|
||||
acceptedTime: Date.now(),
|
||||
callId: 0,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
conversation: {
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
|
@ -27,10 +23,7 @@ const callDetails = {
|
|||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
callDetails,
|
||||
},
|
||||
callState: select(
|
||||
'callState',
|
||||
CallState,
|
||||
|
@ -44,6 +37,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
overrideProps.hasRemoteVideo || false
|
||||
),
|
||||
i18n,
|
||||
joinedAt: Date.now(),
|
||||
me: {
|
||||
color: 'ultramarine' as ColorType,
|
||||
name: 'Morty Smith',
|
||||
|
|
|
@ -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<PropsType> = ({
|
||||
callDetails,
|
||||
callState,
|
||||
conversation,
|
||||
hangUp,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
i18n,
|
||||
joinedAt,
|
||||
me,
|
||||
setLocalAudio,
|
||||
setLocalVideo,
|
||||
|
@ -59,29 +68,17 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
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<number | null>(null);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
|
@ -100,15 +97,15 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
}, [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<PropsType> = ({
|
|||
|
||||
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<PropsType> = ({
|
|||
)}
|
||||
>
|
||||
<div className="module-calling__header--header-name">
|
||||
{callDetails.title}
|
||||
{conversation.title}
|
||||
</div>
|
||||
{renderHeaderMessage(i18n, callState, acceptedDuration)}
|
||||
<div className="module-calling-tools">
|
||||
|
@ -205,7 +198,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
ref={remoteVideoRef}
|
||||
/>
|
||||
) : (
|
||||
renderAvatar(i18n, callDetails)
|
||||
renderAvatar(i18n, conversation)
|
||||
)}
|
||||
<div className="module-ongoing-call__footer">
|
||||
{/* This layout-only element is not ideal.
|
||||
|
@ -233,7 +226,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
buttonType={CallingButtonType.HANG_UP}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
hangUp({ callId });
|
||||
hangUp({ conversationId: conversation.id });
|
||||
}}
|
||||
tooltipDistance={24}
|
||||
/>
|
||||
|
@ -269,16 +262,22 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
|
||||
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 (
|
||||
<div className="module-ongoing-call__remote-video-disabled">
|
||||
<Avatar
|
||||
|
|
|
@ -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,
|
||||
|
@ -39,7 +35,7 @@ const camera = {
|
|||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): 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 (
|
||||
<CallingLobby
|
||||
{...props}
|
||||
callDetails={{
|
||||
...callDetails,
|
||||
conversation={{
|
||||
...conversation,
|
||||
avatarPath: 'https://www.stevensegallery.com/600/600',
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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<MediaDeviceInfo>;
|
||||
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 = ({
|
|||
<div className="module-calling__container">
|
||||
<div className="module-calling__header">
|
||||
<div className="module-calling__header--header-name">
|
||||
{callDetails.title}
|
||||
{conversation.title}
|
||||
</div>
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall ? (
|
||||
|
@ -136,8 +132,8 @@ export const CallingLobby = ({
|
|||
<video ref={localVideoRef} autoPlay />
|
||||
) : (
|
||||
<CallBackgroundBlur
|
||||
avatarPath={callDetails.avatarPath}
|
||||
color={callDetails.color}
|
||||
avatarPath={conversation.avatarPath}
|
||||
color={conversation.color}
|
||||
>
|
||||
<div className="module-calling-lobby__video-off--icon" />
|
||||
<span className="module-calling-lobby__video-off--text">
|
||||
|
|
|
@ -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,
|
||||
|
@ -28,7 +24,7 @@ const callDetails = {
|
|||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
callDetails: overrideProps.callDetails || callDetails,
|
||||
conversation: overrideProps.conversation || conversation,
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
hasRemoteVideo: boolean(
|
||||
|
@ -50,8 +46,8 @@ story.add('Default', () => {
|
|||
|
||||
story.add('Contact (with avatar)', () => {
|
||||
const props = createProps({
|
||||
callDetails: {
|
||||
...callDetails,
|
||||
conversation: {
|
||||
...conversation,
|
||||
avatarPath: 'https://www.fillmurray.com/64/64',
|
||||
},
|
||||
});
|
||||
|
@ -60,8 +56,8 @@ story.add('Contact (with avatar)', () => {
|
|||
|
||||
story.add('Contact (no color)', () => {
|
||||
const props = createProps({
|
||||
callDetails: {
|
||||
...callDetails,
|
||||
conversation: {
|
||||
...conversation,
|
||||
color: undefined,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,28 +3,33 @@
|
|||
|
||||
import React from 'react';
|
||||
import {
|
||||
CallDetailsType,
|
||||
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(
|
||||
callDetails: CallDetailsType,
|
||||
i18n: LocalizerType
|
||||
): JSX.Element {
|
||||
const {
|
||||
{
|
||||
avatarPath,
|
||||
color,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
} = callDetails;
|
||||
|
||||
}: {
|
||||
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}>
|
||||
|
@ -48,7 +53,15 @@ function renderAvatar(
|
|||
}
|
||||
|
||||
export type PropsType = {
|
||||
callDetails: CallDetailsType;
|
||||
conversation: {
|
||||
id: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
};
|
||||
hangUp: (_: HangUpType) => void;
|
||||
hasLocalVideo: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
|
@ -64,7 +77,7 @@ const PIP_DEFAULT_Y = 56;
|
|||
const PIP_PADDING = 8;
|
||||
|
||||
export const CallingPip = ({
|
||||
callDetails,
|
||||
conversation,
|
||||
hangUp,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
|
@ -204,7 +217,7 @@ export const CallingPip = ({
|
|||
ref={remoteVideoRef}
|
||||
/>
|
||||
) : (
|
||||
renderAvatar(callDetails, i18n)
|
||||
renderAvatar(conversation, i18n)
|
||||
)}
|
||||
{hasLocalVideo ? (
|
||||
<video
|
||||
|
@ -219,7 +232,7 @@ export const CallingPip = ({
|
|||
aria-label={i18n('calling__hangup')}
|
||||
className="module-calling-pip__button--hangup"
|
||||
onClick={() => {
|
||||
hangUp({ callId: callDetails.callId });
|
||||
hangUp({ conversationId: conversation.id });
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
|
|
|
@ -15,11 +15,13 @@ const i18n = setupI18n('en', enMessages);
|
|||
|
||||
const defaultProps = {
|
||||
acceptCall: action('accept-call'),
|
||||
callDetails: {
|
||||
call: {
|
||||
conversationId: 'fake-conversation-id',
|
||||
callId: 0,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
|
||||
},
|
||||
conversation: {
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
contactColor: 'ultramarine' as ColorType,
|
||||
|
@ -33,24 +35,15 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Incoming Call Bar (no call details)',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Incoming Call Bar (video)',
|
||||
props: {
|
||||
callDetails: {
|
||||
...defaultProps.callDetails,
|
||||
isVideoCall: true,
|
||||
},
|
||||
},
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Incoming Call Bar (audio)',
|
||||
props: {
|
||||
callDetails: {
|
||||
...defaultProps.callDetails,
|
||||
call: {
|
||||
...defaultProps.call,
|
||||
isVideoCall: false,
|
||||
},
|
||||
},
|
||||
|
@ -69,10 +62,13 @@ storiesOf('Components/IncomingCallBar', module)
|
|||
return (
|
||||
<IncomingCallBar
|
||||
{...defaultProps}
|
||||
callDetails={{
|
||||
...defaultProps.callDetails,
|
||||
color,
|
||||
call={{
|
||||
...defaultProps.call,
|
||||
isVideoCall,
|
||||
}}
|
||||
conversation={{
|
||||
...defaultProps.conversation,
|
||||
color,
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -6,17 +6,25 @@ import Tooltip from 'react-tooltip-lite';
|
|||
import { Avatar } from './Avatar';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import {
|
||||
AcceptCallType,
|
||||
CallDetailsType,
|
||||
DeclineCallType,
|
||||
} from '../state/ducks/calling';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { AcceptCallType, DeclineCallType } from '../state/ducks/calling';
|
||||
|
||||
export type PropsType = {
|
||||
acceptCall: (_: AcceptCallType) => void;
|
||||
callDetails?: CallDetailsType;
|
||||
declineCall: (_: DeclineCallType) => void;
|
||||
i18n: LocalizerType;
|
||||
call: {
|
||||
isVideoCall: boolean;
|
||||
};
|
||||
conversation: {
|
||||
id: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CallButtonProps = {
|
||||
|
@ -54,23 +62,21 @@ const CallButton = ({
|
|||
|
||||
export const IncomingCallBar = ({
|
||||
acceptCall,
|
||||
callDetails,
|
||||
declineCall,
|
||||
i18n,
|
||||
call,
|
||||
conversation,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (!callDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isVideoCall } = call;
|
||||
const {
|
||||
id: conversationId,
|
||||
avatarPath,
|
||||
callId,
|
||||
color,
|
||||
title,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
} = callDetails;
|
||||
} = conversation;
|
||||
|
||||
return (
|
||||
<div className="module-incoming-call">
|
||||
|
@ -103,21 +109,17 @@ export const IncomingCallBar = ({
|
|||
dir="auto"
|
||||
className="module-incoming-call__contact--message-text"
|
||||
>
|
||||
{i18n(
|
||||
callDetails.isVideoCall
|
||||
? 'incomingVideoCall'
|
||||
: 'incomingAudioCall'
|
||||
)}
|
||||
{i18n(isVideoCall ? 'incomingVideoCall' : 'incomingAudioCall')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-incoming-call__actions">
|
||||
{callDetails.isVideoCall ? (
|
||||
{isVideoCall ? (
|
||||
<>
|
||||
<CallButton
|
||||
classSuffix="decline"
|
||||
onClick={() => {
|
||||
declineCall({ callId });
|
||||
declineCall({ conversationId });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('declineCall')}
|
||||
|
@ -125,7 +127,7 @@ export const IncomingCallBar = ({
|
|||
<CallButton
|
||||
classSuffix="accept-video-as-audio"
|
||||
onClick={() => {
|
||||
acceptCall({ callId, asVideoCall: false });
|
||||
acceptCall({ conversationId, asVideoCall: false });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('acceptCallWithoutVideo')}
|
||||
|
@ -133,7 +135,7 @@ export const IncomingCallBar = ({
|
|||
<CallButton
|
||||
classSuffix="accept-video"
|
||||
onClick={() => {
|
||||
acceptCall({ callId, asVideoCall: true });
|
||||
acceptCall({ conversationId, asVideoCall: true });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('acceptCall')}
|
||||
|
@ -144,7 +146,7 @@ export const IncomingCallBar = ({
|
|||
<CallButton
|
||||
classSuffix="decline"
|
||||
onClick={() => {
|
||||
declineCall({ callId });
|
||||
declineCall({ conversationId });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('declineCall')}
|
||||
|
@ -152,7 +154,7 @@ export const IncomingCallBar = ({
|
|||
<CallButton
|
||||
classSuffix="accept-audio"
|
||||
onClick={() => {
|
||||
acceptCall({ callId, asVideoCall: false });
|
||||
acceptCall({ conversationId, asVideoCall: false });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('acceptCall')}
|
||||
|
|
|
@ -21,10 +21,7 @@ import {
|
|||
UserId,
|
||||
} from 'ringrtc';
|
||||
|
||||
import {
|
||||
ActionsType as UxActionsType,
|
||||
CallDetailsType,
|
||||
} from '../state/ducks/calling';
|
||||
import { ActionsType as UxActionsType } from '../state/ducks/calling';
|
||||
import { EnvelopeClass } from '../textsecure.d';
|
||||
import { AudioDevice, MediaDeviceSettings } from '../types/Calling';
|
||||
import { ConversationModel } from '../models/conversations';
|
||||
|
@ -48,9 +45,13 @@ export class CallingClass {
|
|||
|
||||
private deviceReselectionTimer?: NodeJS.Timeout;
|
||||
|
||||
private callsByConversation: { [conversationId: string]: Call };
|
||||
|
||||
constructor() {
|
||||
this.videoCapturer = new GumVideoCapturer(640, 480, 30);
|
||||
this.videoRenderer = new CanvasVideoRenderer();
|
||||
|
||||
this.callsByConversation = {};
|
||||
}
|
||||
|
||||
initialize(uxActions: UxActionsType): void {
|
||||
|
@ -101,12 +102,8 @@ export class CallingClass {
|
|||
|
||||
window.log.info('CallingClass.startCallingLobby(): Starting lobby');
|
||||
this.uxActions.showCallLobby({
|
||||
callDetails: {
|
||||
...conversationProps,
|
||||
callId: undefined,
|
||||
isIncoming: false,
|
||||
conversationId: conversationProps.id,
|
||||
isVideoCall,
|
||||
},
|
||||
});
|
||||
|
||||
await this.startDeviceReselectionTimer();
|
||||
|
@ -124,7 +121,8 @@ export class CallingClass {
|
|||
|
||||
async startOutgoingCall(
|
||||
conversationId: string,
|
||||
isVideoCall: boolean
|
||||
hasLocalAudio: boolean,
|
||||
hasLocalVideo: boolean
|
||||
): Promise<void> {
|
||||
window.log.info('CallingClass.startCallingLobby()');
|
||||
|
||||
|
@ -147,7 +145,7 @@ export class CallingClass {
|
|||
return;
|
||||
}
|
||||
|
||||
const haveMediaPermissions = await this.requestPermissions(isVideoCall);
|
||||
const haveMediaPermissions = await this.requestPermissions(hasLocalVideo);
|
||||
if (!haveMediaPermissions) {
|
||||
window.log.info('Permissions were denied, new call not allowed.');
|
||||
this.stopCallingLobby();
|
||||
|
@ -171,24 +169,38 @@ export class CallingClass {
|
|||
// from the RingRTC before we lookup the ICE servers.
|
||||
const call = RingRTC.startOutgoingCall(
|
||||
remoteUserId,
|
||||
isVideoCall,
|
||||
hasLocalVideo,
|
||||
this.localDeviceId,
|
||||
callSettings
|
||||
);
|
||||
|
||||
await this.startDeviceReselectionTimer();
|
||||
RingRTC.setOutgoingAudio(call.callId, hasLocalAudio);
|
||||
RingRTC.setVideoCapturer(call.callId, this.videoCapturer);
|
||||
RingRTC.setVideoRenderer(call.callId, this.videoRenderer);
|
||||
this.attachToCall(conversation, call);
|
||||
|
||||
this.uxActions.outgoingCall({
|
||||
callDetails: this.getAcceptedCallDetails(conversation, call),
|
||||
conversationId: conversation.id,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
});
|
||||
|
||||
await this.startDeviceReselectionTimer();
|
||||
}
|
||||
|
||||
async accept(callId: CallId, asVideoCall: boolean): Promise<void> {
|
||||
private getCallIdForConversation(conversationId: string): undefined | CallId {
|
||||
return this.callsByConversation[conversationId]?.callId;
|
||||
}
|
||||
|
||||
async accept(conversationId: string, asVideoCall: boolean): Promise<void> {
|
||||
window.log.info('CallingClass.accept()');
|
||||
|
||||
const callId = this.getCallIdForConversation(conversationId);
|
||||
if (!callId) {
|
||||
window.log.warn('Trying to accept a non-existent call');
|
||||
return;
|
||||
}
|
||||
|
||||
const haveMediaPermissions = await this.requestPermissions(asVideoCall);
|
||||
if (haveMediaPermissions) {
|
||||
await this.startDeviceReselectionTimer();
|
||||
|
@ -201,23 +213,47 @@ export class CallingClass {
|
|||
}
|
||||
}
|
||||
|
||||
decline(callId: CallId): void {
|
||||
decline(conversationId: string): void {
|
||||
window.log.info('CallingClass.decline()');
|
||||
|
||||
const callId = this.getCallIdForConversation(conversationId);
|
||||
if (!callId) {
|
||||
window.log.warn('Trying to decline a non-existent call');
|
||||
return;
|
||||
}
|
||||
|
||||
RingRTC.decline(callId);
|
||||
}
|
||||
|
||||
hangup(callId: CallId): void {
|
||||
hangup(conversationId: string): void {
|
||||
window.log.info('CallingClass.hangup()');
|
||||
|
||||
const callId = this.getCallIdForConversation(conversationId);
|
||||
if (!callId) {
|
||||
window.log.warn('Trying to hang up a non-existent call');
|
||||
return;
|
||||
}
|
||||
|
||||
RingRTC.hangup(callId);
|
||||
}
|
||||
|
||||
setOutgoingAudio(callId: CallId, enabled: boolean): void {
|
||||
setOutgoingAudio(conversationId: string, enabled: boolean): void {
|
||||
const callId = this.getCallIdForConversation(conversationId);
|
||||
if (!callId) {
|
||||
window.log.warn('Trying to set outgoing audio for a non-existent call');
|
||||
return;
|
||||
}
|
||||
|
||||
RingRTC.setOutgoingAudio(callId, enabled);
|
||||
}
|
||||
|
||||
setOutgoingVideo(callId: CallId, enabled: boolean): void {
|
||||
setOutgoingVideo(conversationId: string, enabled: boolean): void {
|
||||
const callId = this.getCallIdForConversation(conversationId);
|
||||
if (!callId) {
|
||||
window.log.warn('Trying to set outgoing video for a non-existent call');
|
||||
return;
|
||||
}
|
||||
|
||||
RingRTC.setOutgoingVideo(callId, enabled);
|
||||
}
|
||||
|
||||
|
@ -673,8 +709,9 @@ export class CallingClass {
|
|||
|
||||
this.attachToCall(conversation, call);
|
||||
|
||||
this.uxActions.incomingCall({
|
||||
callDetails: this.getAcceptedCallDetails(conversation, call),
|
||||
this.uxActions.receiveIncomingCall({
|
||||
conversationId: conversation.id,
|
||||
isVideoCall: call.isVideoCall,
|
||||
});
|
||||
|
||||
window.log.info('CallingClass.handleIncomingCall(): Proceeding');
|
||||
|
@ -699,6 +736,8 @@ export class CallingClass {
|
|||
}
|
||||
|
||||
private attachToCall(conversation: ConversationModel, call: Call): void {
|
||||
this.callsByConversation[conversation.id] = call;
|
||||
|
||||
const { uxActions } = this;
|
||||
if (!uxActions) {
|
||||
return;
|
||||
|
@ -709,23 +748,29 @@ export class CallingClass {
|
|||
// eslint-disable-next-line no-param-reassign
|
||||
call.handleStateChanged = () => {
|
||||
if (call.state === CallState.Accepted) {
|
||||
acceptedTime = Date.now();
|
||||
acceptedTime = acceptedTime || Date.now();
|
||||
} else if (call.state === CallState.Ended) {
|
||||
this.addCallHistoryForEndedCall(conversation, call, acceptedTime);
|
||||
this.stopDeviceReselectionTimer();
|
||||
this.lastMediaDeviceSettings = undefined;
|
||||
delete this.callsByConversation[conversation.id];
|
||||
}
|
||||
uxActions.callStateChange({
|
||||
conversationId: conversation.id,
|
||||
acceptedTime,
|
||||
callState: call.state,
|
||||
callDetails: this.getAcceptedCallDetails(conversation, call),
|
||||
callEndedReason: call.endedReason,
|
||||
isIncoming: call.isIncoming,
|
||||
isVideoCall: call.isVideoCall,
|
||||
title: conversation.getTitle(),
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
call.handleRemoteVideoEnabled = () => {
|
||||
uxActions.remoteVideoChange({
|
||||
remoteVideoEnabled: call.remoteVideoEnabled,
|
||||
conversationId: conversation.id,
|
||||
hasVideo: call.remoteVideoEnabled,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -797,21 +842,6 @@ export class CallingClass {
|
|||
};
|
||||
}
|
||||
|
||||
private getAcceptedCallDetails(
|
||||
conversation: ConversationModel,
|
||||
call: Call
|
||||
): CallDetailsType {
|
||||
const conversationProps = conversation.format();
|
||||
|
||||
return {
|
||||
...conversationProps,
|
||||
acceptedTime: Date.now(),
|
||||
callId: call.callId,
|
||||
isIncoming: call.isIncoming,
|
||||
isVideoCall: call.isVideoCall,
|
||||
};
|
||||
}
|
||||
|
||||
private addCallHistoryForEndedCall(
|
||||
conversation: ConversationModel,
|
||||
call: Call,
|
||||
|
|
|
@ -3,16 +3,18 @@
|
|||
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { CallEndedReason } from 'ringrtc';
|
||||
import { has, omit } from 'lodash';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import { notify } from '../../services/notify';
|
||||
import { calling } from '../../services/calling';
|
||||
import { StateType as RootStateType } from '../reducer';
|
||||
import { getActiveCall } from '../selectors/calling';
|
||||
import {
|
||||
CallingDeviceType,
|
||||
CallState,
|
||||
ChangeIODevicePayloadType,
|
||||
MediaDeviceSettings,
|
||||
} from '../../types/Calling';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { callingTones } from '../../util/callingTones';
|
||||
import { requestCameraPermissions } from '../../util/callingPermissions';
|
||||
import {
|
||||
|
@ -22,76 +24,82 @@ import {
|
|||
|
||||
// State
|
||||
|
||||
export type CallId = unknown;
|
||||
|
||||
export type CallDetailsType = {
|
||||
acceptedTime?: number;
|
||||
callId: CallId;
|
||||
isIncoming: boolean;
|
||||
isVideoCall: boolean;
|
||||
|
||||
id: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type CallingStateType = MediaDeviceSettings & {
|
||||
callDetails?: CallDetailsType;
|
||||
export interface DirectCallStateType {
|
||||
conversationId: string;
|
||||
callState?: CallState;
|
||||
callEndedReason?: CallEndedReason;
|
||||
isIncoming: boolean;
|
||||
isVideoCall: boolean;
|
||||
hasRemoteVideo?: boolean;
|
||||
}
|
||||
|
||||
export interface ActiveCallStateType {
|
||||
conversationId: string;
|
||||
joinedAt?: number;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
participantsList: boolean;
|
||||
pip: boolean;
|
||||
settingsDialogOpen: boolean;
|
||||
}
|
||||
|
||||
export type CallingStateType = MediaDeviceSettings & {
|
||||
callsByConversation: { [conversationId: string]: DirectCallStateType };
|
||||
activeCallState?: ActiveCallStateType;
|
||||
};
|
||||
|
||||
export type AcceptCallType = {
|
||||
callId: CallId;
|
||||
conversationId: string;
|
||||
asVideoCall: boolean;
|
||||
};
|
||||
|
||||
export type CallStateChangeType = {
|
||||
conversationId: string;
|
||||
acceptedTime?: number;
|
||||
callState: CallState;
|
||||
callDetails: CallDetailsType;
|
||||
callEndedReason?: CallEndedReason;
|
||||
isIncoming: boolean;
|
||||
isVideoCall: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type DeclineCallType = {
|
||||
callId: CallId;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
export type HangUpType = {
|
||||
callId: CallId;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
export type IncomingCallType = {
|
||||
callDetails: CallDetailsType;
|
||||
conversationId: string;
|
||||
isVideoCall: boolean;
|
||||
};
|
||||
|
||||
export type OutgoingCallType = {
|
||||
callDetails: CallDetailsType;
|
||||
export type StartCallType = {
|
||||
conversationId: string;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
};
|
||||
|
||||
export type RemoteVideoChangeType = {
|
||||
remoteVideoEnabled: boolean;
|
||||
conversationId: string;
|
||||
hasVideo: boolean;
|
||||
};
|
||||
|
||||
export type SetLocalAudioType = {
|
||||
callId?: CallId;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type SetLocalVideoType = {
|
||||
callId?: CallId;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type ShowCallLobbyType = {
|
||||
conversationId: string;
|
||||
isVideoCall: boolean;
|
||||
};
|
||||
|
||||
export type SetLocalPreviewType = {
|
||||
element: React.RefObject<HTMLVideoElement> | undefined;
|
||||
};
|
||||
|
@ -102,19 +110,6 @@ export type SetRendererCanvasType = {
|
|||
|
||||
// Helpers
|
||||
|
||||
export function isCallActive({
|
||||
callDetails,
|
||||
callState,
|
||||
}: CallingStateType): boolean {
|
||||
return Boolean(
|
||||
callDetails &&
|
||||
((!callDetails.isIncoming &&
|
||||
(callState === CallState.Prering || callState === CallState.Ringing)) ||
|
||||
callState === CallState.Accepted ||
|
||||
callState === CallState.Reconnecting)
|
||||
);
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
const ACCEPT_CALL_PENDING = 'calling/ACCEPT_CALL_PENDING';
|
||||
|
@ -129,7 +124,7 @@ const INCOMING_CALL = 'calling/INCOMING_CALL';
|
|||
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
||||
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
||||
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||
const SET_LOCAL_AUDIO = 'calling/SET_LOCAL_AUDIO';
|
||||
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||
const START_CALL = 'calling/START_CALL';
|
||||
const TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS';
|
||||
|
@ -147,7 +142,7 @@ type CancelCallActionType = {
|
|||
|
||||
type CallLobbyActionType = {
|
||||
type: 'calling/SHOW_CALL_LOBBY';
|
||||
payload: OutgoingCallType;
|
||||
payload: ShowCallLobbyType;
|
||||
};
|
||||
|
||||
type CallStateChangeFulfilledActionType = {
|
||||
|
@ -182,7 +177,7 @@ type IncomingCallActionType = {
|
|||
|
||||
type OutgoingCallActionType = {
|
||||
type: 'calling/OUTGOING_CALL';
|
||||
payload: OutgoingCallType;
|
||||
payload: StartCallType;
|
||||
};
|
||||
|
||||
type RefreshIODevicesActionType = {
|
||||
|
@ -196,7 +191,7 @@ type RemoteVideoChangeActionType = {
|
|||
};
|
||||
|
||||
type SetLocalAudioActionType = {
|
||||
type: 'calling/SET_LOCAL_AUDIO';
|
||||
type: 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||
payload: SetLocalAudioType;
|
||||
};
|
||||
|
||||
|
@ -205,8 +200,14 @@ type SetLocalVideoFulfilledActionType = {
|
|||
payload: SetLocalVideoType;
|
||||
};
|
||||
|
||||
type ShowCallLobbyActionType = {
|
||||
type: 'calling/SHOW_CALL_LOBBY';
|
||||
payload: ShowCallLobbyType;
|
||||
};
|
||||
|
||||
type StartCallActionType = {
|
||||
type: 'calling/START_CALL';
|
||||
payload: StartCallType;
|
||||
};
|
||||
|
||||
type ToggleParticipantsActionType = {
|
||||
|
@ -236,6 +237,7 @@ export type CallingActionType =
|
|||
| RemoteVideoChangeActionType
|
||||
| SetLocalAudioActionType
|
||||
| SetLocalVideoFulfilledActionType
|
||||
| ShowCallLobbyActionType
|
||||
| StartCallActionType
|
||||
| ToggleParticipantsActionType
|
||||
| TogglePipActionType
|
||||
|
@ -253,7 +255,7 @@ function acceptCall(
|
|||
});
|
||||
|
||||
try {
|
||||
await calling.accept(payload.callId, payload.asVideoCall);
|
||||
await calling.accept(payload.conversationId, payload.asVideoCall);
|
||||
} catch (err) {
|
||||
window.log.error(`Failed to acceptCall: ${err.stack}`);
|
||||
}
|
||||
|
@ -269,11 +271,10 @@ function callStateChange(
|
|||
CallStateChangeFulfilledActionType
|
||||
> {
|
||||
return async dispatch => {
|
||||
const { callDetails, callState } = payload;
|
||||
const { isIncoming } = callDetails;
|
||||
const { callState, isIncoming, title, isVideoCall } = payload;
|
||||
if (callState === CallState.Ringing && isIncoming) {
|
||||
await callingTones.playRingtone();
|
||||
await showCallNotification(callDetails);
|
||||
await showCallNotification(title, isVideoCall);
|
||||
bounceAppIconStart();
|
||||
}
|
||||
if (callState !== CallState.Ringing) {
|
||||
|
@ -315,12 +316,14 @@ function changeIODevice(
|
|||
};
|
||||
}
|
||||
|
||||
async function showCallNotification(callDetails: CallDetailsType) {
|
||||
async function showCallNotification(
|
||||
title: string,
|
||||
isVideoCall: boolean
|
||||
): Promise<void> {
|
||||
const canNotify = await window.getCallSystemNotification();
|
||||
if (!canNotify) {
|
||||
return;
|
||||
}
|
||||
const { title, isVideoCall } = callDetails;
|
||||
notify({
|
||||
title,
|
||||
icon: isVideoCall
|
||||
|
@ -352,7 +355,7 @@ function cancelCall(): CancelCallActionType {
|
|||
}
|
||||
|
||||
function declineCall(payload: DeclineCallType): DeclineCallActionType {
|
||||
calling.decline(payload.callId);
|
||||
calling.decline(payload.conversationId);
|
||||
|
||||
return {
|
||||
type: DECLINE_CALL,
|
||||
|
@ -361,7 +364,7 @@ function declineCall(payload: DeclineCallType): DeclineCallActionType {
|
|||
}
|
||||
|
||||
function hangUp(payload: HangUpType): HangUpActionType {
|
||||
calling.hangup(payload.callId);
|
||||
calling.hangup(payload.conversationId);
|
||||
|
||||
return {
|
||||
type: HANG_UP,
|
||||
|
@ -369,14 +372,16 @@ function hangUp(payload: HangUpType): HangUpActionType {
|
|||
};
|
||||
}
|
||||
|
||||
function incomingCall(payload: IncomingCallType): IncomingCallActionType {
|
||||
function receiveIncomingCall(
|
||||
payload: IncomingCallType
|
||||
): IncomingCallActionType {
|
||||
return {
|
||||
type: INCOMING_CALL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function outgoingCall(payload: OutgoingCallType): OutgoingCallActionType {
|
||||
function outgoingCall(payload: StartCallType): OutgoingCallActionType {
|
||||
callingTones.playRingtone();
|
||||
|
||||
return {
|
||||
|
@ -419,25 +424,32 @@ function setRendererCanvas(
|
|||
};
|
||||
}
|
||||
|
||||
function setLocalAudio(payload: SetLocalAudioType): SetLocalAudioActionType {
|
||||
if (payload.callId) {
|
||||
calling.setOutgoingAudio(payload.callId, payload.enabled);
|
||||
function setLocalAudio(
|
||||
payload: SetLocalAudioType
|
||||
): ThunkAction<void, RootStateType, unknown, SetLocalAudioActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const { conversationId } = getActiveCall(getState().calling) || {};
|
||||
if (conversationId) {
|
||||
calling.setOutgoingAudio(conversationId, payload.enabled);
|
||||
}
|
||||
|
||||
return {
|
||||
type: SET_LOCAL_AUDIO,
|
||||
dispatch({
|
||||
type: SET_LOCAL_AUDIO_FULFILLED,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setLocalVideo(
|
||||
payload: SetLocalVideoType
|
||||
): ThunkAction<void, RootStateType, unknown, SetLocalVideoFulfilledActionType> {
|
||||
return async dispatch => {
|
||||
return async (dispatch, getState) => {
|
||||
let enabled: boolean;
|
||||
if (await requestCameraPermissions()) {
|
||||
if (payload.callId) {
|
||||
calling.setOutgoingVideo(payload.callId, payload.enabled);
|
||||
const { conversationId, callState } =
|
||||
getActiveCall(getState().calling) || {};
|
||||
if (conversationId && callState) {
|
||||
calling.setOutgoingVideo(conversationId, payload.enabled);
|
||||
} else if (payload.enabled) {
|
||||
calling.enableLocalCamera();
|
||||
} else {
|
||||
|
@ -458,22 +470,23 @@ function setLocalVideo(
|
|||
};
|
||||
}
|
||||
|
||||
function showCallLobby(payload: OutgoingCallType): CallLobbyActionType {
|
||||
function showCallLobby(payload: ShowCallLobbyType): CallLobbyActionType {
|
||||
return {
|
||||
type: SHOW_CALL_LOBBY,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function startCall(payload: OutgoingCallType): StartCallActionType {
|
||||
const { callDetails } = payload;
|
||||
window.Signal.Services.calling.startOutgoingCall(
|
||||
callDetails.id,
|
||||
callDetails.isVideoCall
|
||||
function startCall(payload: StartCallType): StartCallActionType {
|
||||
calling.startOutgoingCall(
|
||||
payload.conversationId,
|
||||
payload.hasLocalAudio,
|
||||
payload.hasLocalVideo
|
||||
);
|
||||
|
||||
return {
|
||||
type: START_CALL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -503,7 +516,7 @@ export const actions = {
|
|||
closeNeedPermissionScreen,
|
||||
declineCall,
|
||||
hangUp,
|
||||
incomingCall,
|
||||
receiveIncomingCall,
|
||||
outgoingCall,
|
||||
refreshIODevices,
|
||||
remoteVideoChange,
|
||||
|
@ -527,18 +540,24 @@ export function getEmptyState(): CallingStateType {
|
|||
availableCameras: [],
|
||||
availableMicrophones: [],
|
||||
availableSpeakers: [],
|
||||
callDetails: undefined,
|
||||
callState: undefined,
|
||||
callEndedReason: undefined,
|
||||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
hasRemoteVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
selectedCamera: undefined,
|
||||
selectedMicrophone: undefined,
|
||||
selectedSpeaker: undefined,
|
||||
settingsDialogOpen: false,
|
||||
|
||||
callsByConversation: {},
|
||||
activeCallState: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function removeConversationFromState(
|
||||
state: CallingStateType,
|
||||
conversationId: string
|
||||
): CallingStateType {
|
||||
return {
|
||||
...(conversationId === state.activeCallState?.conversationId
|
||||
? omit(state, 'activeCallState')
|
||||
: state),
|
||||
callsByConversation: omit(state.callsByConversation, conversationId),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -546,53 +565,126 @@ export function reducer(
|
|||
state: CallingStateType = getEmptyState(),
|
||||
action: CallingActionType
|
||||
): CallingStateType {
|
||||
const { callsByConversation } = state;
|
||||
|
||||
if (action.type === SHOW_CALL_LOBBY) {
|
||||
return {
|
||||
...state,
|
||||
callDetails: action.payload.callDetails,
|
||||
callState: undefined,
|
||||
callsByConversation: {
|
||||
...callsByConversation,
|
||||
[action.payload.conversationId]: {
|
||||
conversationId: action.payload.conversationId,
|
||||
isIncoming: false,
|
||||
isVideoCall: action.payload.isVideoCall,
|
||||
},
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: action.payload.callDetails.isVideoCall,
|
||||
hasLocalVideo: action.payload.isVideoCall,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === START_CALL) {
|
||||
return {
|
||||
...state,
|
||||
callsByConversation: {
|
||||
...callsByConversation,
|
||||
[action.payload.conversationId]: {
|
||||
conversationId: action.payload.conversationId,
|
||||
callState: CallState.Prering,
|
||||
isIncoming: false,
|
||||
isVideoCall: action.payload.hasLocalVideo,
|
||||
},
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: action.payload.hasLocalAudio,
|
||||
hasLocalVideo: action.payload.hasLocalVideo,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === ACCEPT_CALL_PENDING) {
|
||||
if (!has(state.callsByConversation, action.payload.conversationId)) {
|
||||
window.log.warn('Unable to accept a non-existent call');
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeCallState: {
|
||||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: action.payload.asVideoCall,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
action.type === CANCEL_CALL ||
|
||||
action.type === DECLINE_CALL ||
|
||||
action.type === HANG_UP ||
|
||||
action.type === CLOSE_NEED_PERMISSION_SCREEN
|
||||
) {
|
||||
return getEmptyState();
|
||||
if (!state.activeCallState) {
|
||||
window.log.warn('No active call to remove');
|
||||
return state;
|
||||
}
|
||||
return removeConversationFromState(
|
||||
state,
|
||||
state.activeCallState.conversationId
|
||||
);
|
||||
}
|
||||
|
||||
if (action.type === DECLINE_CALL) {
|
||||
return removeConversationFromState(state, action.payload.conversationId);
|
||||
}
|
||||
|
||||
if (action.type === INCOMING_CALL) {
|
||||
return {
|
||||
...state,
|
||||
callDetails: action.payload.callDetails,
|
||||
callsByConversation: {
|
||||
...callsByConversation,
|
||||
[action.payload.conversationId]: {
|
||||
conversationId: action.payload.conversationId,
|
||||
callState: CallState.Prering,
|
||||
isIncoming: true,
|
||||
isVideoCall: action.payload.isVideoCall,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === OUTGOING_CALL) {
|
||||
return {
|
||||
...state,
|
||||
callDetails: action.payload.callDetails,
|
||||
callsByConversation: {
|
||||
...callsByConversation,
|
||||
[action.payload.conversationId]: {
|
||||
conversationId: action.payload.conversationId,
|
||||
callState: CallState.Prering,
|
||||
isIncoming: false,
|
||||
isVideoCall: action.payload.hasLocalVideo,
|
||||
},
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: action.payload.hasLocalAudio,
|
||||
hasLocalVideo: action.payload.hasLocalVideo,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -604,33 +696,91 @@ export function reducer(
|
|||
action.payload.callEndedReason !==
|
||||
CallEndedReason.RemoteHangupNeedPermission
|
||||
) {
|
||||
return getEmptyState();
|
||||
return removeConversationFromState(state, action.payload.conversationId);
|
||||
}
|
||||
|
||||
const call = getOwn(
|
||||
state.callsByConversation,
|
||||
action.payload.conversationId
|
||||
);
|
||||
if (!call) {
|
||||
window.log.warn('Cannot update state for non-existent call');
|
||||
return state;
|
||||
}
|
||||
|
||||
let activeCallState: undefined | ActiveCallStateType;
|
||||
if (
|
||||
state.activeCallState?.conversationId === action.payload.conversationId
|
||||
) {
|
||||
activeCallState = {
|
||||
...state.activeCallState,
|
||||
joinedAt: action.payload.acceptedTime,
|
||||
};
|
||||
} else {
|
||||
({ activeCallState } = state);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
callsByConversation: {
|
||||
...callsByConversation,
|
||||
[action.payload.conversationId]: {
|
||||
...call,
|
||||
callState: action.payload.callState,
|
||||
callEndedReason: action.payload.callEndedReason,
|
||||
},
|
||||
},
|
||||
activeCallState,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === REMOTE_VIDEO_CHANGE) {
|
||||
const { conversationId, hasVideo } = action.payload;
|
||||
const call = getOwn(state.callsByConversation, conversationId);
|
||||
if (!call) {
|
||||
window.log.warn('Cannot update remote video for a non-existent call');
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
hasRemoteVideo: action.payload.remoteVideoEnabled,
|
||||
callsByConversation: {
|
||||
...callsByConversation,
|
||||
[conversationId]: {
|
||||
...call,
|
||||
hasRemoteVideo: hasVideo,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_LOCAL_AUDIO) {
|
||||
if (action.type === SET_LOCAL_AUDIO_FULFILLED) {
|
||||
if (!state.activeCallState) {
|
||||
window.log.warn('Cannot set local audio with no active call');
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeCallState: {
|
||||
...state.activeCallState,
|
||||
hasLocalAudio: action.payload.enabled,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_LOCAL_VIDEO_FULFILLED) {
|
||||
if (!state.activeCallState) {
|
||||
window.log.warn('Cannot set local video with no active call');
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeCallState: {
|
||||
...state.activeCallState,
|
||||
hasLocalVideo: action.payload.enabled,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -674,23 +824,52 @@ export function reducer(
|
|||
}
|
||||
|
||||
if (action.type === TOGGLE_SETTINGS) {
|
||||
const { activeCallState } = state;
|
||||
if (!activeCallState) {
|
||||
window.log.warn('Cannot toggle settings when there is no active call');
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
settingsDialogOpen: !state.settingsDialogOpen,
|
||||
activeCallState: {
|
||||
...activeCallState,
|
||||
settingsDialogOpen: !activeCallState.settingsDialogOpen,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_PARTICIPANTS) {
|
||||
const { activeCallState } = state;
|
||||
if (!activeCallState) {
|
||||
window.log.warn(
|
||||
'Cannot toggle participants list when there is no active call'
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
participantsList: !state.participantsList,
|
||||
activeCallState: {
|
||||
...activeCallState,
|
||||
participantsList: !activeCallState.participantsList,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_PIP) {
|
||||
const { activeCallState } = state;
|
||||
if (!activeCallState) {
|
||||
window.log.warn('Cannot toggle PiP when there is no active call');
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
pip: !state.pip,
|
||||
activeCallState: {
|
||||
...activeCallState,
|
||||
pip: !activeCallState.pip,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,3 +5,10 @@ export type NoopActionType = {
|
|||
type: 'NOOP';
|
||||
payload: null;
|
||||
};
|
||||
|
||||
export function noopAction(): NoopActionType {
|
||||
return {
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
};
|
||||
}
|
||||
|
|
33
ts/state/selectors/calling.ts
Normal file
33
ts/state/selectors/calling.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { CallingStateType } from '../ducks/calling';
|
||||
import { CallState } from '../../types/Calling';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
|
||||
const getActiveCallState = (state: CallingStateType) => state.activeCallState;
|
||||
|
||||
const getCallsByConversation = (state: CallingStateType) =>
|
||||
state.callsByConversation;
|
||||
|
||||
// In theory, there could be multiple incoming calls. In practice, neither RingRTC nor the
|
||||
// UI are ready to handle this.
|
||||
export const getIncomingCall = createSelector(
|
||||
getCallsByConversation,
|
||||
callsByConversation =>
|
||||
Object.values(callsByConversation).find(
|
||||
call => call.isIncoming && call.callState === CallState.Ringing
|
||||
)
|
||||
);
|
||||
|
||||
export const getActiveCall = createSelector(
|
||||
getActiveCallState,
|
||||
getCallsByConversation,
|
||||
(activeCallState, callsByConversation) =>
|
||||
activeCallState &&
|
||||
getOwn(callsByConversation, activeCallState.conversationId)
|
||||
);
|
||||
|
||||
export const isCallActive = createSelector(getActiveCall, Boolean);
|
|
@ -5,7 +5,8 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { CallManager } from '../../components/CallManager';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
import { getMe, getConversationSelector } from '../selectors/conversations';
|
||||
import { getActiveCall, getIncomingCall } from '../selectors/calling';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
@ -16,15 +17,63 @@ function renderDeviceSelection(): JSX.Element {
|
|||
return <SmartCallingDeviceSelection />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const mapStateToActiveCallProp = (state: StateType) => {
|
||||
const { calling } = state;
|
||||
const { activeCallState } = calling;
|
||||
|
||||
if (!activeCallState) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const call = getActiveCall(calling);
|
||||
if (!call) {
|
||||
window.log.error(
|
||||
'There was an active call state but no corresponding call'
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversation = getConversationSelector(state)(
|
||||
activeCallState.conversationId
|
||||
);
|
||||
if (!conversation) {
|
||||
window.log.error('The active call has no corresponding conversation');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...calling,
|
||||
call,
|
||||
activeCallState,
|
||||
conversation,
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToIncomingCallProp = (state: StateType) => {
|
||||
const call = getIncomingCall(state.calling);
|
||||
if (!call) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversation = getConversationSelector(state)(call.conversationId);
|
||||
if (!conversation) {
|
||||
window.log.error('The incoming call has no corresponding conversation');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
call,
|
||||
conversation,
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType) => ({
|
||||
activeCall: mapStateToActiveCallProp(state),
|
||||
availableCameras: state.calling.availableCameras,
|
||||
i18n: getIntl(state),
|
||||
incomingCall: mapStateToIncomingCallProp(state),
|
||||
me: getMe(state),
|
||||
renderDeviceSelection,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { pick } from 'lodash';
|
|||
import { ConversationHeader } from '../../components/conversation/ConversationHeader';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { StateType } from '../reducer';
|
||||
import { isCallActive } from '../ducks/calling';
|
||||
import { isCallActive } from '../selectors/calling';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
export interface OwnProps {
|
||||
|
|
|
@ -2,28 +2,309 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import {
|
||||
CallDetailsType,
|
||||
actions,
|
||||
getEmptyState,
|
||||
isCallActive,
|
||||
reducer,
|
||||
} from '../../../state/ducks/calling';
|
||||
import * as sinon from 'sinon';
|
||||
import { reducer as rootReducer } from '../../../state/reducer';
|
||||
import { noopAction } from '../../../state/ducks/noop';
|
||||
import { actions, getEmptyState, reducer } from '../../../state/ducks/calling';
|
||||
import { calling as callingService } from '../../../services/calling';
|
||||
import { CallState } from '../../../types/Calling';
|
||||
|
||||
describe('calling duck', () => {
|
||||
const stateWithDirectCall = {
|
||||
...getEmptyState(),
|
||||
callsByConversation: {
|
||||
'fake-direct-call-conversation-id': {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
callState: CallState.Accepted,
|
||||
isIncoming: false,
|
||||
isVideoCall: false,
|
||||
hasRemoteVideo: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const stateWithActiveDirectCall = {
|
||||
...stateWithDirectCall,
|
||||
activeCallState: {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
const stateWithIncomingDirectCall = {
|
||||
...getEmptyState(),
|
||||
callsByConversation: {
|
||||
'fake-direct-call-conversation-id': {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: false,
|
||||
hasRemoteVideo: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getEmptyRootState = () => rootReducer(undefined, noopAction());
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.sandbox = sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
this.sandbox.restore();
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('acceptCall', () => {
|
||||
const { acceptCall } = actions;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.callingServiceAccept = this.sandbox
|
||||
.stub(callingService, 'accept')
|
||||
.resolves();
|
||||
});
|
||||
|
||||
it('dispatches an ACCEPT_CALL_PENDING action', async () => {
|
||||
const dispatch = sinon.spy();
|
||||
|
||||
await acceptCall({
|
||||
conversationId: '123',
|
||||
asVideoCall: true,
|
||||
})(dispatch, getEmptyRootState, null);
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'calling/ACCEPT_CALL_PENDING',
|
||||
payload: {
|
||||
conversationId: '123',
|
||||
asVideoCall: true,
|
||||
},
|
||||
});
|
||||
|
||||
await acceptCall({
|
||||
conversationId: '456',
|
||||
asVideoCall: false,
|
||||
})(dispatch, getEmptyRootState, null);
|
||||
|
||||
sinon.assert.calledTwice(dispatch);
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'calling/ACCEPT_CALL_PENDING',
|
||||
payload: {
|
||||
conversationId: '456',
|
||||
asVideoCall: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('asks the calling service to accept the call', async function test() {
|
||||
const dispatch = sinon.spy();
|
||||
|
||||
await acceptCall({
|
||||
conversationId: '123',
|
||||
asVideoCall: true,
|
||||
})(dispatch, getEmptyRootState, null);
|
||||
|
||||
sinon.assert.calledOnce(this.callingServiceAccept);
|
||||
sinon.assert.calledWith(this.callingServiceAccept, '123', true);
|
||||
|
||||
await acceptCall({
|
||||
conversationId: '456',
|
||||
asVideoCall: false,
|
||||
})(dispatch, getEmptyRootState, null);
|
||||
|
||||
sinon.assert.calledTwice(this.callingServiceAccept);
|
||||
sinon.assert.calledWith(this.callingServiceAccept, '456', false);
|
||||
});
|
||||
|
||||
it('updates the active call state with ACCEPT_CALL_PENDING', async () => {
|
||||
const dispatch = sinon.spy();
|
||||
await acceptCall({
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
asVideoCall: true,
|
||||
})(dispatch, getEmptyRootState, null);
|
||||
const action = dispatch.getCall(0).args[0];
|
||||
|
||||
const result = reducer(stateWithIncomingDirectCall, action);
|
||||
|
||||
assert.deepEqual(result.activeCallState, {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLocalAudio', () => {
|
||||
const { setLocalAudio } = actions;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.callingServiceSetOutgoingAudio = this.sandbox.stub(
|
||||
callingService,
|
||||
'setOutgoingAudio'
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches a SET_LOCAL_AUDIO_FULFILLED action', () => {
|
||||
const dispatch = sinon.spy();
|
||||
|
||||
setLocalAudio({ enabled: true })(dispatch, getEmptyRootState, null);
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWith(dispatch, {
|
||||
type: 'calling/SET_LOCAL_AUDIO_FULFILLED',
|
||||
payload: { enabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the outgoing audio for the active call', function test() {
|
||||
const dispatch = sinon.spy();
|
||||
|
||||
setLocalAudio({ enabled: false })(
|
||||
dispatch,
|
||||
() => ({
|
||||
...getEmptyRootState(),
|
||||
calling: stateWithActiveDirectCall,
|
||||
}),
|
||||
null
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(this.callingServiceSetOutgoingAudio);
|
||||
sinon.assert.calledWith(
|
||||
this.callingServiceSetOutgoingAudio,
|
||||
'fake-direct-call-conversation-id',
|
||||
false
|
||||
);
|
||||
|
||||
setLocalAudio({ enabled: true })(
|
||||
dispatch,
|
||||
() => ({
|
||||
...getEmptyRootState(),
|
||||
calling: stateWithActiveDirectCall,
|
||||
}),
|
||||
null
|
||||
);
|
||||
|
||||
sinon.assert.calledTwice(this.callingServiceSetOutgoingAudio);
|
||||
sinon.assert.calledWith(
|
||||
this.callingServiceSetOutgoingAudio,
|
||||
'fake-direct-call-conversation-id',
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the local audio state with SET_LOCAL_AUDIO_FULFILLED', () => {
|
||||
const dispatch = sinon.spy();
|
||||
setLocalAudio({ enabled: false })(dispatch, getEmptyRootState, null);
|
||||
const action = dispatch.getCall(0).args[0];
|
||||
|
||||
const result = reducer(stateWithActiveDirectCall, action);
|
||||
|
||||
assert.isFalse(result.activeCallState?.hasLocalAudio);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showCallLobby', () => {
|
||||
const { showCallLobby } = actions;
|
||||
|
||||
it('saves the call and makes it active', () => {
|
||||
const result = reducer(
|
||||
getEmptyState(),
|
||||
showCallLobby({
|
||||
conversationId: 'fake-conversation-id',
|
||||
isVideoCall: true,
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
|
||||
conversationId: 'fake-conversation-id',
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
});
|
||||
assert.deepEqual(result.activeCallState, {
|
||||
conversationId: 'fake-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('startCall', () => {
|
||||
const { startCall } = actions;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.callingStartOutgoingCall = this.sandbox.stub(
|
||||
callingService,
|
||||
'startOutgoingCall'
|
||||
);
|
||||
});
|
||||
|
||||
it('asks the calling service to start an outgoing call', function test() {
|
||||
startCall({
|
||||
conversationId: '123',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(this.callingStartOutgoingCall);
|
||||
sinon.assert.calledWith(
|
||||
this.callingStartOutgoingCall,
|
||||
'123',
|
||||
true,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('saves the call and makes it active', () => {
|
||||
const result = reducer(
|
||||
getEmptyState(),
|
||||
startCall({
|
||||
conversationId: 'fake-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
|
||||
conversationId: 'fake-conversation-id',
|
||||
callState: CallState.Prering,
|
||||
isIncoming: false,
|
||||
isVideoCall: false,
|
||||
});
|
||||
assert.deepEqual(result.activeCallState, {
|
||||
conversationId: 'fake-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleSettings', () => {
|
||||
const { toggleSettings } = actions;
|
||||
|
||||
it('toggles the settings dialog', () => {
|
||||
const afterOneToggle = reducer(getEmptyState(), toggleSettings());
|
||||
const afterOneToggle = reducer(
|
||||
stateWithActiveDirectCall,
|
||||
toggleSettings()
|
||||
);
|
||||
const afterTwoToggles = reducer(afterOneToggle, toggleSettings());
|
||||
const afterThreeToggles = reducer(afterTwoToggles, toggleSettings());
|
||||
|
||||
assert.isTrue(afterOneToggle.settingsDialogOpen);
|
||||
assert.isFalse(afterTwoToggles.settingsDialogOpen);
|
||||
assert.isTrue(afterThreeToggles.settingsDialogOpen);
|
||||
assert.isTrue(afterOneToggle.activeCallState?.settingsDialogOpen);
|
||||
assert.isFalse(afterTwoToggles.activeCallState?.settingsDialogOpen);
|
||||
assert.isTrue(afterThreeToggles.activeCallState?.settingsDialogOpen);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -31,16 +312,19 @@ describe('calling duck', () => {
|
|||
const { toggleParticipants } = actions;
|
||||
|
||||
it('toggles the participants list', () => {
|
||||
const afterOneToggle = reducer(getEmptyState(), toggleParticipants());
|
||||
const afterOneToggle = reducer(
|
||||
stateWithActiveDirectCall,
|
||||
toggleParticipants()
|
||||
);
|
||||
const afterTwoToggles = reducer(afterOneToggle, toggleParticipants());
|
||||
const afterThreeToggles = reducer(
|
||||
afterTwoToggles,
|
||||
toggleParticipants()
|
||||
);
|
||||
|
||||
assert.isTrue(afterOneToggle.participantsList);
|
||||
assert.isFalse(afterTwoToggles.participantsList);
|
||||
assert.isTrue(afterThreeToggles.participantsList);
|
||||
assert.isTrue(afterOneToggle.activeCallState?.participantsList);
|
||||
assert.isFalse(afterTwoToggles.activeCallState?.participantsList);
|
||||
assert.isTrue(afterThreeToggles.activeCallState?.participantsList);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -48,111 +332,13 @@ describe('calling duck', () => {
|
|||
const { togglePip } = actions;
|
||||
|
||||
it('toggles the PiP', () => {
|
||||
const afterOneToggle = reducer(getEmptyState(), togglePip());
|
||||
const afterOneToggle = reducer(stateWithActiveDirectCall, togglePip());
|
||||
const afterTwoToggles = reducer(afterOneToggle, togglePip());
|
||||
const afterThreeToggles = reducer(afterTwoToggles, togglePip());
|
||||
|
||||
assert.isTrue(afterOneToggle.pip);
|
||||
assert.isFalse(afterTwoToggles.pip);
|
||||
assert.isTrue(afterThreeToggles.pip);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('isCallActive', () => {
|
||||
const fakeCallDetails: CallDetailsType = {
|
||||
id: 'fake-call',
|
||||
title: 'Fake Call',
|
||||
callId: 123,
|
||||
isIncoming: false,
|
||||
isVideoCall: false,
|
||||
};
|
||||
|
||||
it('returns false if there are no call details', () => {
|
||||
assert.isFalse(isCallActive(getEmptyState()));
|
||||
});
|
||||
|
||||
it('returns false if an incoming call is in a pre-reing state', () => {
|
||||
assert.isFalse(
|
||||
isCallActive({
|
||||
...getEmptyState(),
|
||||
callDetails: {
|
||||
...fakeCallDetails,
|
||||
isIncoming: true,
|
||||
},
|
||||
callState: CallState.Prering,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true if an outgoing call is in a pre-reing state', () => {
|
||||
assert.isTrue(
|
||||
isCallActive({
|
||||
...getEmptyState(),
|
||||
callDetails: {
|
||||
...fakeCallDetails,
|
||||
isIncoming: false,
|
||||
},
|
||||
callState: CallState.Prering,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false if an incoming call is ringing', () => {
|
||||
assert.isFalse(
|
||||
isCallActive({
|
||||
...getEmptyState(),
|
||||
callDetails: {
|
||||
...fakeCallDetails,
|
||||
isIncoming: true,
|
||||
},
|
||||
callState: CallState.Ringing,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true if an outgoing call is ringing', () => {
|
||||
assert.isTrue(
|
||||
isCallActive({
|
||||
...getEmptyState(),
|
||||
callDetails: {
|
||||
...fakeCallDetails,
|
||||
isIncoming: false,
|
||||
},
|
||||
callState: CallState.Ringing,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true if a call is in an accepted state', () => {
|
||||
assert.isTrue(
|
||||
isCallActive({
|
||||
...getEmptyState(),
|
||||
callDetails: fakeCallDetails,
|
||||
callState: CallState.Accepted,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true if a call is in a reconnecting state', () => {
|
||||
assert.isTrue(
|
||||
isCallActive({
|
||||
...getEmptyState(),
|
||||
callDetails: fakeCallDetails,
|
||||
callState: CallState.Reconnecting,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false if a call is in an ended state', () => {
|
||||
assert.isFalse(
|
||||
isCallActive({
|
||||
...getEmptyState(),
|
||||
callDetails: fakeCallDetails,
|
||||
callState: CallState.Ended,
|
||||
})
|
||||
);
|
||||
assert.isTrue(afterOneToggle.activeCallState?.pip);
|
||||
assert.isFalse(afterTwoToggles.activeCallState?.pip);
|
||||
assert.isTrue(afterThreeToggles.activeCallState?.pip);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
106
ts/test-electron/state/selectors/calling_test.ts
Normal file
106
ts/test-electron/state/selectors/calling_test.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CallState } from '../../../types/Calling';
|
||||
import {
|
||||
getIncomingCall,
|
||||
getActiveCall,
|
||||
isCallActive,
|
||||
} from '../../../state/selectors/calling';
|
||||
import { getEmptyState } from '../../../state/ducks/calling';
|
||||
|
||||
describe('state/selectors/calling', () => {
|
||||
const stateWithDirectCall = {
|
||||
...getEmptyState(),
|
||||
callsByConversation: {
|
||||
'fake-direct-call-conversation-id': {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
callState: CallState.Accepted,
|
||||
isIncoming: false,
|
||||
isVideoCall: false,
|
||||
hasRemoteVideo: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const stateWithActiveDirectCall = {
|
||||
...stateWithDirectCall,
|
||||
activeCallState: {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
const stateWithIncomingDirectCall = {
|
||||
...getEmptyState(),
|
||||
callsByConversation: {
|
||||
'fake-direct-call-conversation-id': {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: false,
|
||||
hasRemoteVideo: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('getIncomingCall', () => {
|
||||
it('returns undefined if there are no calls', () => {
|
||||
assert.isUndefined(getIncomingCall(getEmptyState()));
|
||||
});
|
||||
|
||||
it('returns undefined if there is no incoming call', () => {
|
||||
assert.isUndefined(getIncomingCall(stateWithDirectCall));
|
||||
assert.isUndefined(getIncomingCall(stateWithActiveDirectCall));
|
||||
});
|
||||
|
||||
it('returns the incoming call', () => {
|
||||
assert.deepEqual(getIncomingCall(stateWithIncomingDirectCall), {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: false,
|
||||
hasRemoteVideo: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveCall', () => {
|
||||
it('returns undefined if there are no calls', () => {
|
||||
assert.isUndefined(getActiveCall(getEmptyState()));
|
||||
});
|
||||
|
||||
it('returns undefined if there is no active call', () => {
|
||||
assert.isUndefined(getActiveCall(stateWithDirectCall));
|
||||
});
|
||||
|
||||
it('returns the active call', () => {
|
||||
assert.deepEqual(getActiveCall(stateWithActiveDirectCall), {
|
||||
conversationId: 'fake-direct-call-conversation-id',
|
||||
callState: CallState.Accepted,
|
||||
isIncoming: false,
|
||||
isVideoCall: false,
|
||||
hasRemoteVideo: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCallActive', () => {
|
||||
it('returns false if there are no calls', () => {
|
||||
assert.isFalse(isCallActive(getEmptyState()));
|
||||
});
|
||||
|
||||
it('returns false if there is no active call', () => {
|
||||
assert.isFalse(isCallActive(stateWithDirectCall));
|
||||
});
|
||||
|
||||
it('returns true if there is an active call', () => {
|
||||
assert.isTrue(isCallActive(stateWithActiveDirectCall));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -14391,7 +14391,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.js",
|
||||
"line": " const localVideoRef = react_1.useRef(null);",
|
||||
"lineNumber": 44,
|
||||
"lineNumber": 35,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T21:35:52.858Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14400,7 +14400,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.js",
|
||||
"line": " const remoteVideoRef = react_1.useRef(null);",
|
||||
"lineNumber": 45,
|
||||
"lineNumber": 36,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T21:35:52.858Z",
|
||||
"reasonDetail": "Used to get the remote video element for rendering."
|
||||
|
@ -14418,7 +14418,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingLobby.tsx",
|
||||
"line": " const localVideoRef = React.useRef(null);",
|
||||
"lineNumber": 50,
|
||||
"lineNumber": 54,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14427,7 +14427,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.js",
|
||||
"line": " const videoContainerRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 23,
|
||||
"lineNumber": 22,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Element is measured. Its HTML is not used."
|
||||
|
@ -14436,7 +14436,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.js",
|
||||
"line": " const localVideoRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 24,
|
||||
"lineNumber": 23,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14445,7 +14445,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.js",
|
||||
"line": " const remoteVideoRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 25,
|
||||
"lineNumber": 24,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the remote video element for rendering."
|
||||
|
@ -14454,7 +14454,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
"line": " const videoContainerRef = React.useRef(null);",
|
||||
"lineNumber": 76,
|
||||
"lineNumber": 89,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Element is measured. Its HTML is not used."
|
||||
|
@ -14463,7 +14463,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
"line": " const localVideoRef = React.useRef(null);",
|
||||
"lineNumber": 77,
|
||||
"lineNumber": 90,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14472,7 +14472,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
"line": " const remoteVideoRef = React.useRef(null);",
|
||||
"lineNumber": 78,
|
||||
"lineNumber": 91,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the remote video element for rendering."
|
||||
|
|
Loading…
Reference in a new issue