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