2020-06-04 18:16:19 +00:00
|
|
|
import { notify } from '../../services/notify';
|
|
|
|
import { calling, VideoCapturer, VideoRenderer } from '../../services/calling';
|
|
|
|
import { CallState } from '../../types/Calling';
|
|
|
|
import { CanvasVideoRenderer, GumVideoCapturer } from '../../window.d';
|
|
|
|
import { ColorType } from '../../types/Util';
|
|
|
|
import { NoopActionType } from './noop';
|
|
|
|
import { callingTones } from '../../util/callingTones';
|
|
|
|
import { requestCameraPermissions } from '../../util/callingPermissions';
|
|
|
|
import {
|
|
|
|
bounceAppIconStart,
|
|
|
|
bounceAppIconStop,
|
|
|
|
} from '../../shims/bounceAppIcon';
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
|
|
export type CallId = any;
|
|
|
|
|
|
|
|
export type CallDetailsType = {
|
|
|
|
avatarPath?: string;
|
|
|
|
callId: CallId;
|
|
|
|
contactColor?: ColorType;
|
|
|
|
isIncoming: boolean;
|
|
|
|
isVideoCall: boolean;
|
|
|
|
name?: string;
|
|
|
|
phoneNumber: string;
|
|
|
|
profileName?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type CallingStateType = {
|
|
|
|
callDetails?: CallDetailsType;
|
|
|
|
callState?: CallState;
|
|
|
|
hasLocalAudio: boolean;
|
|
|
|
hasLocalVideo: boolean;
|
|
|
|
hasRemoteVideo: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type AcceptCallType = {
|
|
|
|
callId: CallId;
|
|
|
|
asVideoCall: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type CallStateChangeType = {
|
|
|
|
callState: CallState;
|
|
|
|
callDetails: CallDetailsType;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type DeclineCallType = {
|
|
|
|
callId: CallId;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type HangUpType = {
|
|
|
|
callId: CallId;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type IncomingCallType = {
|
|
|
|
callDetails: CallDetailsType;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type OutgoingCallType = {
|
|
|
|
callDetails: CallDetailsType;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type RemoteVideoChangeType = {
|
|
|
|
remoteVideoEnabled: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type SetLocalAudioType = {
|
|
|
|
callId: CallId;
|
|
|
|
enabled: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type SetLocalVideoType = {
|
|
|
|
callId: CallId;
|
|
|
|
enabled: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type SetVideoCapturerType = {
|
|
|
|
callId: CallId;
|
|
|
|
capturer: CanvasVideoRenderer | null;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type SetVideoRendererType = {
|
|
|
|
callId: CallId;
|
|
|
|
renderer: GumVideoCapturer | null;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
|
|
|
const ACCEPT_CALL = 'calling/ACCEPT_CALL';
|
|
|
|
const CALL_STATE_CHANGE = 'calling/CALL_STATE_CHANGE';
|
2020-07-10 16:55:23 +00:00
|
|
|
const CALL_STATE_CHANGE_FULFILLED = 'calling/CALL_STATE_CHANGE_FULFILLED';
|
2020-06-04 18:16:19 +00:00
|
|
|
const DECLINE_CALL = 'calling/DECLINE_CALL';
|
|
|
|
const HANG_UP = 'calling/HANG_UP';
|
|
|
|
const INCOMING_CALL = 'calling/INCOMING_CALL';
|
|
|
|
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
|
|
|
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
|
|
|
const SET_LOCAL_AUDIO = 'calling/SET_LOCAL_AUDIO';
|
|
|
|
const SET_LOCAL_VIDEO = 'calling/SET_LOCAL_VIDEO';
|
|
|
|
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
|
|
|
|
|
|
|
type AcceptCallActionType = {
|
|
|
|
type: 'calling/ACCEPT_CALL';
|
|
|
|
payload: AcceptCallType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type CallStateChangeActionType = {
|
|
|
|
type: 'calling/CALL_STATE_CHANGE';
|
2020-07-10 16:55:23 +00:00
|
|
|
payload: Promise<CallStateChangeType>;
|
|
|
|
};
|
|
|
|
|
|
|
|
type CallStateChangeFulfilledActionType = {
|
|
|
|
type: 'calling/CALL_STATE_CHANGE_FULFILLED';
|
2020-06-04 18:16:19 +00:00
|
|
|
payload: CallStateChangeType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type DeclineCallActionType = {
|
|
|
|
type: 'calling/DECLINE_CALL';
|
|
|
|
payload: DeclineCallType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type HangUpActionType = {
|
|
|
|
type: 'calling/HANG_UP';
|
|
|
|
payload: HangUpType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type IncomingCallActionType = {
|
|
|
|
type: 'calling/INCOMING_CALL';
|
|
|
|
payload: IncomingCallType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type OutgoingCallActionType = {
|
|
|
|
type: 'calling/OUTGOING_CALL';
|
|
|
|
payload: OutgoingCallType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type RemoteVideoChangeActionType = {
|
|
|
|
type: 'calling/REMOTE_VIDEO_CHANGE';
|
|
|
|
payload: RemoteVideoChangeType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type SetLocalAudioActionType = {
|
|
|
|
type: 'calling/SET_LOCAL_AUDIO';
|
|
|
|
payload: SetLocalAudioType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type SetLocalVideoActionType = {
|
|
|
|
type: 'calling/SET_LOCAL_VIDEO';
|
|
|
|
payload: Promise<SetLocalVideoType>;
|
|
|
|
};
|
|
|
|
|
|
|
|
type SetLocalVideoFulfilledActionType = {
|
|
|
|
type: 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
|
|
|
payload: SetLocalVideoType;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type CallingActionType =
|
|
|
|
| AcceptCallActionType
|
|
|
|
| CallStateChangeActionType
|
2020-07-10 16:55:23 +00:00
|
|
|
| CallStateChangeFulfilledActionType
|
2020-06-04 18:16:19 +00:00
|
|
|
| DeclineCallActionType
|
|
|
|
| HangUpActionType
|
|
|
|
| IncomingCallActionType
|
|
|
|
| OutgoingCallActionType
|
|
|
|
| RemoteVideoChangeActionType
|
|
|
|
| SetLocalAudioActionType
|
|
|
|
| SetLocalVideoActionType
|
|
|
|
| SetLocalVideoFulfilledActionType;
|
|
|
|
|
|
|
|
// Action Creators
|
|
|
|
|
|
|
|
function acceptCall(
|
|
|
|
payload: AcceptCallType
|
|
|
|
): AcceptCallActionType | NoopActionType {
|
|
|
|
// tslint:disable-next-line no-floating-promises
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
await calling.accept(payload.callId, payload.asVideoCall);
|
|
|
|
} catch (err) {
|
|
|
|
window.log.error(`Failed to acceptCall: ${err.stack}`);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: ACCEPT_CALL,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function callStateChange(
|
|
|
|
payload: CallStateChangeType
|
|
|
|
): CallStateChangeActionType {
|
2020-07-10 16:55:23 +00:00
|
|
|
return {
|
|
|
|
type: CALL_STATE_CHANGE,
|
|
|
|
payload: doCallStateChange(payload),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async function doCallStateChange(
|
|
|
|
payload: CallStateChangeType
|
|
|
|
): Promise<CallStateChangeType> {
|
2020-06-04 18:16:19 +00:00
|
|
|
const { callDetails, callState } = payload;
|
|
|
|
const { isIncoming } = callDetails;
|
|
|
|
if (callState === CallState.Ringing && isIncoming) {
|
2020-07-10 16:55:23 +00:00
|
|
|
await callingTones.playRingtone();
|
|
|
|
await showCallNotification(callDetails);
|
2020-06-04 18:16:19 +00:00
|
|
|
bounceAppIconStart();
|
|
|
|
}
|
|
|
|
if (callState !== CallState.Ringing) {
|
|
|
|
callingTones.stopRingtone();
|
|
|
|
bounceAppIconStop();
|
|
|
|
}
|
|
|
|
if (callState === CallState.Ended) {
|
|
|
|
// tslint:disable-next-line no-floating-promises
|
|
|
|
callingTones.playEndCall();
|
|
|
|
}
|
2020-07-10 16:55:23 +00:00
|
|
|
return payload;
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function showCallNotification(callDetails: CallDetailsType) {
|
|
|
|
const canNotify = await window.getCallSystemNotification();
|
|
|
|
if (!canNotify) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const { name, phoneNumber, profileName, isVideoCall } = callDetails;
|
|
|
|
notify({
|
|
|
|
platform: window.platform,
|
|
|
|
title: `${name || phoneNumber} ${profileName || ''}`,
|
|
|
|
icon: isVideoCall
|
|
|
|
? 'images/icons/v2/video-solid-24.svg'
|
|
|
|
: 'images/icons/v2/phone-right-solid-24.svg',
|
|
|
|
message: window.i18n(
|
|
|
|
isVideoCall ? 'incomingVideoCall' : 'incomingAudioCall'
|
|
|
|
),
|
|
|
|
onNotificationClick: () => {
|
|
|
|
window.showWindow();
|
|
|
|
},
|
|
|
|
silent: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function declineCall(payload: DeclineCallType): DeclineCallActionType {
|
|
|
|
calling.decline(payload.callId);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: DECLINE_CALL,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function hangUp(payload: HangUpType): HangUpActionType {
|
|
|
|
calling.hangup(payload.callId);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: HANG_UP,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function incomingCall(payload: IncomingCallType): IncomingCallActionType {
|
|
|
|
return {
|
|
|
|
type: INCOMING_CALL,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function outgoingCall(payload: OutgoingCallType): OutgoingCallActionType {
|
|
|
|
// tslint:disable-next-line no-floating-promises
|
|
|
|
callingTones.playRingtone();
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: OUTGOING_CALL,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function remoteVideoChange(
|
|
|
|
payload: RemoteVideoChangeType
|
|
|
|
): RemoteVideoChangeActionType {
|
|
|
|
return {
|
|
|
|
type: REMOTE_VIDEO_CHANGE,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function setVideoCapturer(payload: SetVideoCapturerType): NoopActionType {
|
|
|
|
calling.setVideoCapturer(payload.callId, payload.capturer as VideoCapturer);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'NOOP',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function setVideoRenderer(payload: SetVideoRendererType): NoopActionType {
|
|
|
|
calling.setVideoRenderer(payload.callId, payload.renderer as VideoRenderer);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'NOOP',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function setLocalAudio(payload: SetLocalAudioType): SetLocalAudioActionType {
|
|
|
|
calling.setOutgoingAudio(payload.callId, payload.enabled);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: SET_LOCAL_AUDIO,
|
|
|
|
payload,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function setLocalVideo(payload: SetLocalVideoType): SetLocalVideoActionType {
|
|
|
|
return {
|
|
|
|
type: SET_LOCAL_VIDEO,
|
|
|
|
payload: doSetLocalVideo(payload),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async function doSetLocalVideo(
|
|
|
|
payload: SetLocalVideoType
|
|
|
|
): Promise<SetLocalVideoType> {
|
|
|
|
if (await requestCameraPermissions()) {
|
|
|
|
calling.setOutgoingVideo(payload.callId, payload.enabled);
|
|
|
|
return payload;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...payload,
|
|
|
|
enabled: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export const actions = {
|
|
|
|
acceptCall,
|
|
|
|
callStateChange,
|
|
|
|
declineCall,
|
|
|
|
hangUp,
|
|
|
|
incomingCall,
|
|
|
|
outgoingCall,
|
|
|
|
remoteVideoChange,
|
|
|
|
setVideoCapturer,
|
|
|
|
setVideoRenderer,
|
|
|
|
setLocalAudio,
|
|
|
|
setLocalVideo,
|
|
|
|
};
|
|
|
|
|
|
|
|
export type ActionsType = typeof actions;
|
|
|
|
|
|
|
|
// Reducer
|
|
|
|
|
|
|
|
function getEmptyState(): CallingStateType {
|
|
|
|
return {
|
|
|
|
callDetails: undefined,
|
|
|
|
callState: undefined,
|
|
|
|
hasLocalAudio: false,
|
|
|
|
hasLocalVideo: false,
|
|
|
|
hasRemoteVideo: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function reducer(
|
|
|
|
state: CallingStateType = getEmptyState(),
|
|
|
|
action: CallingActionType
|
|
|
|
): CallingStateType {
|
|
|
|
if (action.type === ACCEPT_CALL) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
hasLocalAudio: true,
|
|
|
|
hasLocalVideo: action.payload.asVideoCall,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === DECLINE_CALL || action.type === HANG_UP) {
|
|
|
|
return getEmptyState();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === INCOMING_CALL) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
callDetails: action.payload.callDetails,
|
|
|
|
callState: CallState.Prering,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === OUTGOING_CALL) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
callDetails: action.payload.callDetails,
|
|
|
|
callState: CallState.Prering,
|
|
|
|
hasLocalAudio: true,
|
|
|
|
hasLocalVideo: action.payload.callDetails.isVideoCall,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-07-10 16:55:23 +00:00
|
|
|
if (action.type === CALL_STATE_CHANGE_FULFILLED) {
|
2020-06-04 18:16:19 +00:00
|
|
|
if (action.payload.callState === CallState.Ended) {
|
|
|
|
return getEmptyState();
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
callState: action.payload.callState,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === REMOTE_VIDEO_CHANGE) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
hasRemoteVideo: action.payload.remoteVideoEnabled,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === SET_LOCAL_AUDIO) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
hasLocalAudio: action.payload.enabled,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === SET_LOCAL_VIDEO_FULFILLED) {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
hasLocalVideo: action.payload.enabled,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return state;
|
|
|
|
}
|