Merge branch 'webrtc-calls' of https://github.com/oxen-io/session-desktop into webrtc-calls

pull/1947/head
Warrick Corfe-Tan 4 years ago
commit 5becf6be04

@ -443,5 +443,9 @@
"messageDeletedPlaceholder": "This message has been deleted", "messageDeletedPlaceholder": "This message has been deleted",
"messageDeleted": "Message deleted", "messageDeleted": "Message deleted",
"surveyTitle": "Take our Session Survey", "surveyTitle": "Take our Session Survey",
"goToOurSurvey": "Go to our survey" "goToOurSurvey": "Go to our survey",
"incomingCall": "Incoming call",
"accept": "Accept",
"decline": "Decline",
"endCall": "End call"
} }

@ -102,6 +102,7 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-contexify": "5.0.0", "react-contexify": "5.0.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-emoji": "^0.5.0", "react-emoji": "^0.5.0",
"react-emoji-render": "^1.2.4", "react-emoji-render": "^1.2.4",
"react-h5-audio-player": "^3.2.0", "react-h5-audio-player": "^3.2.0",

@ -1,20 +1,32 @@
import React, { useState } from 'react'; import React, { useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import Draggable from 'react-draggable';
// tslint:disable-next-line: no-submodule-imports
import useMountedState from 'react-use/lib/useMountedState';
import styled from 'styled-components'; import styled from 'styled-components';
import _ from 'underscore'; import _ from 'underscore';
import { getConversationController } from '../../../session/conversations/ConversationController';
import { CallManager } from '../../../session/utils'; import { CallManager } from '../../../session/utils';
import {
getHasIncomingCall,
getHasIncomingCallFrom,
getHasOngoingCall,
getHasOngoingCallWith,
} from '../../../state/selectors/conversations';
import { SessionButton, SessionButtonColor } from '../SessionButton'; import { SessionButton, SessionButtonColor } from '../SessionButton';
import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionWrapperModal } from '../SessionWrapperModal';
export const CallWindow = styled.div` export const CallWindow = styled.div`
position: absolute; position: absolute;
z-index: 9; z-index: 9;
padding: 2rem; padding: 1rem;
top: 50vh; top: 50vh;
left: 50vw; left: 50vw;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--color-modal-background);
border: var(--session-border);
`; `;
// similar styling to modal header // similar styling to modal header
@ -22,7 +34,7 @@ const CallWindowHeader = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-self: center;
padding: $session-margin-lg; padding: $session-margin-lg;
@ -33,138 +45,123 @@ const CallWindowHeader = styled.div`
font-weight: 700; font-weight: 700;
`; `;
// TODO: Add proper styling for this const VideoContainer = styled.div`
const VideoContainer = styled.video` position: relative;
width: 200px; max-height: 60vh;
height: 200px; `;
const VideoContainerRemote = styled.video`
max-height: inherit;
`;
const VideoContainerLocal = styled.video`
max-height: 45%;
max-width: 45%;
position: absolute;
bottom: 0;
right: 0;
`; `;
const CallWindowInner = styled.div` const CallWindowInner = styled.div`
position: relative;
background-color: pink;
border: 1px solid #d3d3d3;
text-align: center; text-align: center;
padding: 2rem; padding: 1rem;
display: flex;
flex-direction: column;
`; `;
const CallWindowControls = styled.div` const CallWindowControls = styled.div`
position: absolute;
top: 100%;
left: 0;
width: 100%;
/* background: green; */
padding: 5px; padding: 5px;
transform: translateY(-100%);
`; `;
// type WindowPositionType = { // TODO:
// top: string; /**
// left: string; * Add mute input, deafen, end call, possibly add person to call
// } | null; * duration - look at how duration calculated for recording.
*/
type CallStateType = 'connecting' | 'ongoing' | 'incoming' | null;
const fakeCaller = '054774a456f15c7aca42fe8d245983549000311aaebcf58ce246250c41fe227676';
export const CallContainer = () => { export const CallContainer = () => {
const conversations = getConversationController().getConversations(); const hasIncomingCall = useSelector(getHasIncomingCall);
const incomingCallProps = useSelector(getHasIncomingCallFrom);
// TODO: const ongoingCallProps = useSelector(getHasOngoingCallWith);
/** const hasOngoingCall = useSelector(getHasOngoingCall);
* Add mute input, deafen, end call, possibly add person to call
* duration - look at how duration calculated for recording. const ongoingOrIncomingPubkey = ongoingCallProps?.id || incomingCallProps?.id;
*/ const videoRefRemote = useRef<any>();
const videoRefLocal = useRef<any>();
const [connectionState, setConnectionState] = useState<CallStateType>('incoming'); const mountedState = useMountedState();
// const [callWindowPosition, setCallWindowPosition] = useState<WindowPositionType>(null)
useEffect(() => {
// picking a conversation at random to test with CallManager.setVideoEventsListener(
const foundConvo = conversations.find(convo => convo.id === fakeCaller); (localStream: MediaStream | null, remoteStream: MediaStream | null) => {
if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
if (!foundConvo) { videoRefLocal.current.srcObject = localStream;
throw new Error('fakeconvo not found'); videoRefRemote.current.srcObject = remoteStream;
} }
foundConvo.callState = 'incoming'; }
console.warn('foundConvo: ', foundConvo); );
const firstCallingConvo = _.first(conversations.filter(convo => convo.callState !== undefined)); return () => {
CallManager.setVideoEventsListener(null);
};
}, []);
//#region input handlers //#region input handlers
const handleAcceptIncomingCall = async () => { const handleAcceptIncomingCall = async () => {
console.warn('accept call'); if (incomingCallProps?.id) {
await CallManager.USER_acceptIncomingCallRequest(incomingCallProps.id);
if (firstCallingConvo) {
setConnectionState('connecting');
firstCallingConvo.callState = 'connecting';
await CallManager.USER_acceptIncomingCallRequest(fakeCaller);
// some delay
setConnectionState('ongoing');
firstCallingConvo.callState = 'ongoing';
} }
// set conversationState = setting up
}; };
const handleDeclineIncomingCall = async () => { const handleDeclineIncomingCall = async () => {
// set conversation.callState = null or undefined
// close the modal // close the modal
if (firstCallingConvo) { if (incomingCallProps?.id) {
firstCallingConvo.callState = undefined; await CallManager.USER_rejectIncomingCallRequest(incomingCallProps.id);
} }
console.warn('declined call');
await CallManager.USER_rejectIncomingCallRequest(fakeCaller);
}; };
const handleEndCall = async () => { const handleEndCall = async () => {
// call method to end call connection // call method to end call connection
console.warn('ending the call'); if (ongoingOrIncomingPubkey) {
await CallManager.USER_rejectIncomingCallRequest(fakeCaller); await CallManager.USER_rejectIncomingCallRequest(ongoingOrIncomingPubkey);
setConnectionState(null); }
}; };
const handleMouseDown = () => {
// reposition call window
};
//#endregion //#endregion
if (connectionState === null) { if (!hasOngoingCall && !hasIncomingCall) {
return null; return null;
} }
return ( if (hasOngoingCall && ongoingCallProps) {
<> return (
{connectionState === 'connecting' ? 'connecting...' : null} <Draggable handle=".dragHandle">
{connectionState === 'ongoing' ? ( <CallWindow className="dragHandle">
<CallWindow onMouseDown={handleMouseDown}> <CallWindowHeader>Call with: {ongoingCallProps.name}</CallWindowHeader>
<CallWindowInner> <CallWindowInner>
<CallWindowHeader> <VideoContainer>
{firstCallingConvo ? firstCallingConvo.getName() : 'Group name not found'} <VideoContainerRemote ref={videoRefRemote} autoPlay={true} />
</CallWindowHeader> <VideoContainerLocal ref={videoRefLocal} autoPlay={true} />
<VideoContainer /> </VideoContainer>
<CallWindowControls>
<SessionButton text={'end call'} onClick={handleEndCall} />
</CallWindowControls>
</CallWindowInner> </CallWindowInner>
<CallWindowControls>
<SessionButton text={window.i18n('endCall')} onClick={handleEndCall} />
</CallWindowControls>
</CallWindow> </CallWindow>
) : null} </Draggable>
);
{!connectionState ? ( }
<SessionWrapperModal title={'incoming call'}>'none'</SessionWrapperModal>
) : null} if (hasIncomingCall) {
return (
{connectionState === 'incoming' ? ( <SessionWrapperModal title={window.i18n('incomingCall')}>
<SessionWrapperModal title={'incoming call'}> <div className="session-modal__button-group">
<div className="session-modal__button-group"> <SessionButton text={window.i18n('decline')} onClick={handleDeclineIncomingCall} />
<SessionButton text={'decline'} onClick={handleDeclineIncomingCall} /> <SessionButton
<SessionButton text={window.i18n('accept')}
text={'accept'} onClick={handleAcceptIncomingCall}
onClick={handleAcceptIncomingCall} buttonColor={SessionButtonColor.Green}
buttonColor={SessionButtonColor.Green} />
/> </div>
</div> </SessionWrapperModal>
</SessionWrapperModal> );
) : null} }
</> // display spinner while connecting
); return null;
}; };

@ -28,8 +28,7 @@ import {
} from '../../../interactions/conversationInteractions'; } from '../../../interactions/conversationInteractions';
import { SessionButtonColor } from '../SessionButton'; import { SessionButtonColor } from '../SessionButton';
import { getTimerOptions } from '../../../state/selectors/timerOptions'; import { getTimerOptions } from '../../../state/selectors/timerOptions';
import { ToastUtils } from '../../../session/utils'; import { CallManager, ToastUtils } from '../../../session/utils';
import { getConversationById } from '../../../data/data';
const maxNumberOfPinnedConversations = 5; const maxNumberOfPinnedConversations = 5;
@ -321,38 +320,32 @@ export function getMarkAllReadMenuItem(conversationId: string): JSX.Element | nu
export function getStartCallMenuItem(conversationId: string): JSX.Element | null { export function getStartCallMenuItem(conversationId: string): JSX.Element | null {
// TODO: possibly conditionally show options? // TODO: possibly conditionally show options?
const callOptions = [ // const callOptions = [
{ // {
name: 'Video call', // name: 'Video call',
value: 'video-call', // value: 'video-call',
}, // },
{ // // {
name: 'Audio call', // // name: 'Audio call',
value: 'audio-call', // // value: 'audio-call',
}, // // },
]; // ];
return ( return (
<Submenu label={'Start call'}> <Item
{callOptions.map(item => ( onClick={async () => {
<Item // TODO: either pass param to callRecipient or call different call methods based on item selected.
key={item.value} const convo = getConversationController().get(conversationId);
onClick={async () => { if (convo) {
// TODO: either pass param to callRecipient or call different call methods based on item selected. convo.callState = 'connecting';
const convo = await getConversationById(conversationId); await convo.commit();
if (convo) {
// window?.libsession?.Utils.CallManager.USER_callRecipient( await CallManager.USER_callRecipient(convo.id);
// '054774a456f15c7aca42fe8d245983549000311aaebcf58ce246250c41fe227676' }
// ); }}
window?.libsession?.Utils.CallManager.USER_callRecipient(convo.id); >
convo.callState = 'connecting'; {'video call'}
} </Item>
}}
>
{item.name}
</Item>
))}
</Submenu>
); );
} }

@ -177,6 +177,8 @@ export const fillConvoAttributesWithDefaults = (
}); });
}; };
export type CallState = 'offering' | 'incoming' | 'connecting' | 'ongoing' | 'none' | undefined;
export class ConversationModel extends Backbone.Model<ConversationAttributes> { export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public updateLastMessage: () => any; public updateLastMessage: () => any;
public throttledBumpTyping: any; public throttledBumpTyping: any;
@ -184,7 +186,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise<void>; public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise<void>;
public initialPromise: any; public initialPromise: any;
public callState: 'offering' | 'incoming' | 'connecting' | 'ongoing' | 'none' | undefined; public callState: CallState;
private typingRefreshTimer?: NodeJS.Timeout | null; private typingRefreshTimer?: NodeJS.Timeout | null;
private typingPauseTimer?: NodeJS.Timeout | null; private typingPauseTimer?: NodeJS.Timeout | null;
@ -440,6 +442,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const left = !!this.get('left'); const left = !!this.get('left');
const expireTimer = this.get('expireTimer'); const expireTimer = this.get('expireTimer');
const currentNotificationSetting = this.get('triggerNotificationsFor'); const currentNotificationSetting = this.get('triggerNotificationsFor');
const callState = this.callState;
// to reduce the redux store size, only set fields which cannot be undefined // to reduce the redux store size, only set fields which cannot be undefined
// for instance, a boolean can usually be not set if false, etc // for instance, a boolean can usually be not set if false, etc
@ -544,6 +547,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
text: lastMessageText, text: lastMessageText,
}; };
} }
if (callState) {
toRet.callState = callState;
}
return toRet; return toRet;
} }

@ -46,7 +46,7 @@ export async function handleCallMessage(
} }
await removeFromCache(envelope); await removeFromCache(envelope);
await CallManager.handleOfferCallMessage(sender, callMessage); CallManager.handleOfferCallMessage(sender, callMessage);
return; return;
} }

@ -17,8 +17,9 @@ import { storeOnNode } from '../snode_api/SNodeAPI';
import { getSwarmFor } from '../snode_api/snodePool'; import { getSwarmFor } from '../snode_api/snodePool';
import { firstTrue } from '../utils/Promise'; import { firstTrue } from '../utils/Promise';
import { MessageSender } from '.'; import { MessageSender } from '.';
import { getConversationById, getMessageById } from '../../../ts/data/data'; import { getMessageById } from '../../../ts/data/data';
import { SNodeAPI } from '../snode_api'; import { SNodeAPI } from '../snode_api';
import { getConversationController } from '../conversations';
const DEFAULT_CONNECTIONS = 3; const DEFAULT_CONNECTIONS = 3;
@ -173,7 +174,7 @@ export async function TEST_sendMessageToSnode(
throw new window.textsecure.EmptySwarmError(pubKey, 'Ran out of swarm nodes to query'); throw new window.textsecure.EmptySwarmError(pubKey, 'Ran out of swarm nodes to query');
} }
const conversation = await getConversationById(pubKey); const conversation = getConversationController().get(pubKey);
const isClosedGroup = conversation?.isClosedGroup(); const isClosedGroup = conversation?.isClosedGroup();
// If message also has a sync message, save that hash. Otherwise save the hash from the regular message send i.e. only closed groups in this case. // If message also has a sync message, save that hash. Otherwise save the hash from the regular message send i.e. only closed groups in this case.

@ -1,25 +1,25 @@
import _ from 'lodash'; import _ from 'lodash';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import {
answerCall,
callConnected,
endCall,
incomingCall,
startingCallWith,
} from '../../state/ducks/conversations';
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { ed25519Str } from '../onions/onionPath'; import { ed25519Str } from '../onions/onionPath';
import { getMessageQueue } from '../sending'; import { getMessageQueue } from '../sending';
import { PubKey } from '../types'; import { PubKey } from '../types';
const incomingCall = ({ sender }: { sender: string }) => { type CallManagerListener =
return { type: 'incomingCall', payload: sender }; | ((localStream: MediaStream | null, remoteStream: MediaStream | null) => void)
}; | null;
const endCall = ({ sender }: { sender: string }) => { let videoEventsListener: CallManagerListener;
return { type: 'endCall', payload: sender };
}; export function setVideoEventsListener(listener: CallManagerListener) {
const answerCall = ({ sender, sdps }: { sender: string; sdps: Array<string> }) => { videoEventsListener = listener;
return { }
type: 'answerCall',
payload: {
sender,
sdps,
},
};
};
/** /**
* This field stores all the details received by a sender about a call in separate messages. * This field stores all the details received by a sender about a call in separate messages.
@ -51,7 +51,7 @@ const configuration = {
// tslint:disable-next-line: function-name // tslint:disable-next-line: function-name
export async function USER_callRecipient(recipient: string) { export async function USER_callRecipient(recipient: string) {
window?.log?.info(`starting call with ${ed25519Str(recipient)}..`); window?.log?.info(`starting call with ${ed25519Str(recipient)}..`);
window.inboxStore?.dispatch(startingCallWith({ pubkey: recipient }));
if (peerConnection) { if (peerConnection) {
window.log.info('closing existing peerconnection'); window.log.info('closing existing peerconnection');
peerConnection.close(); peerConnection.close();
@ -59,18 +59,26 @@ export async function USER_callRecipient(recipient: string) {
} }
peerConnection = new RTCPeerConnection(configuration); peerConnection = new RTCPeerConnection(configuration);
const mediaDevices = await openMediaDevices(); let mediaDevices: any;
mediaDevices.getTracks().map(track => { try {
window.log.info('USER_callRecipient adding track: ', track); const mediaDevices = await openMediaDevices();
peerConnection?.addTrack(track, mediaDevices); mediaDevices.getTracks().map(track => {
}); window.log.info('USER_callRecipient adding track: ', track);
peerConnection?.addTrack(track, mediaDevices);
});
} catch (err) {
console.error('Failed to open media devices. Check camera and mic app permissions');
// TODO: implement toast popup
}
peerConnection.addEventListener('connectionstatechange', _event => { peerConnection.addEventListener('connectionstatechange', _event => {
window.log.info('peerConnection?.connectionState:', peerConnection?.connectionState); window.log.info('peerConnection?.connectionState caller :', peerConnection?.connectionState);
if (peerConnection?.connectionState === 'connected') { if (peerConnection?.connectionState === 'connected') {
// Peers connected! window.inboxStore?.dispatch(callConnected({ pubkey: recipient }));
} }
}); });
peerConnection.addEventListener('ontrack', event => {
console.warn('ontrack:', event);
});
peerConnection.addEventListener('icecandidate', event => { peerConnection.addEventListener('icecandidate', event => {
// window.log.warn('event.candidate', event.candidate); // window.log.warn('event.candidate', event.candidate);
@ -79,6 +87,53 @@ export async function USER_callRecipient(recipient: string) {
void iceSenderDebouncer(recipient); void iceSenderDebouncer(recipient);
} }
}); });
// peerConnection.addEventListener('negotiationneeded', async event => {
peerConnection.onnegotiationneeded = async event => {
console.warn('negotiationneeded:', event);
try {
makingOffer = true;
const offerDescription = await peerConnection?.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
if (!offerDescription) {
console.error('Failed to create offer for negotiation');
return;
}
await peerConnection?.setLocalDescription(offerDescription);
if (!offerDescription || !offerDescription.sdp || !offerDescription.sdp.length) {
// window.log.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`);
console.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`);
return;
}
const callOfferMessage = new CallMessage({
timestamp: Date.now(),
type: SignalService.CallMessage.Type.OFFER,
sdps: [offerDescription.sdp],
});
window.log.info('sending OFFER MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callOfferMessage);
} catch (err) {
console.error(err);
window.log?.error(`Error on handling negotiation needed ${err}`);
} finally {
makingOffer = false;
}
};
const remoteStream = new MediaStream();
if (videoEventsListener) {
videoEventsListener(mediaDevices, remoteStream);
}
peerConnection.addEventListener('track', event => {
if (videoEventsListener) {
videoEventsListener(mediaDevices, remoteStream);
}
remoteStream.addTrack(event.track);
});
const offerDescription = await peerConnection.createOffer({ const offerDescription = await peerConnection.createOffer({
offerToReceiveAudio: true, offerToReceiveAudio: true,
@ -175,6 +230,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
); );
return; return;
} }
window.inboxStore?.dispatch(answerCall({ pubkey: fromSender }));
if (peerConnection) { if (peerConnection) {
window.log.info('closing existing peerconnection'); window.log.info('closing existing peerconnection');
@ -184,42 +240,40 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
peerConnection = new RTCPeerConnection(configuration); peerConnection = new RTCPeerConnection(configuration);
const mediaDevices = await openMediaDevices(); const mediaDevices = await openMediaDevices();
mediaDevices.getTracks().map(track => { mediaDevices.getTracks().map(track => {
window.log.info('USER_acceptIncomingCallRequest adding track ', track); // window.log.info('USER_acceptIncomingCallRequest adding track ', track);
peerConnection?.addTrack(track, mediaDevices); peerConnection?.addTrack(track, mediaDevices);
}); });
peerConnection.addEventListener('icecandidateerror', event => { const remoteStream = new MediaStream();
console.warn('icecandidateerror:', event);
});
peerConnection.addEventListener('icecandidate', event => { peerConnection.addEventListener('icecandidate', event => {
console.warn('icecandidateerror:', event); console.warn('icecandidateerror:', event);
// TODO: ICE stuff
// signaler.send({candidate}); // probably event.candidate // signaler.send({candidate}); // probably event.candidate
}); });
peerConnection.addEventListener('negotiationneeded', async event => {
console.warn('negotiationneeded:', event);
try {
makingOffer = true;
await peerConnection?.setLocalDescription();
// SignalService.CallMessage.Type.OFFER
// signaler.send({ description: pc.localDescription });
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
});
peerConnection.addEventListener('signalingstatechange', event => { peerConnection.addEventListener('signalingstatechange', event => {
console.warn('signalingstatechange:', event); console.warn('signalingstatechange:', event);
}); });
peerConnection.addEventListener('ontrack', event => { if (videoEventsListener) {
console.warn('ontrack:', event); videoEventsListener(mediaDevices, remoteStream);
}
peerConnection.addEventListener('track', event => {
if (videoEventsListener) {
videoEventsListener(mediaDevices, remoteStream);
}
remoteStream.addTrack(event.track);
}); });
peerConnection.addEventListener('connectionstatechange', _event => { peerConnection.addEventListener('connectionstatechange', _event => {
window.log.info('peerConnection?.connectionState:', peerConnection?.connectionState, _event); window.log.info(
'peerConnection?.connectionState recipient:',
peerConnection?.connectionState,
'with: ',
fromSender
);
if (peerConnection?.connectionState === 'connected') { if (peerConnection?.connectionState === 'connected') {
// Peers connected! window.inboxStore?.dispatch(callConnected({ pubkey: fromSender }));
} }
}); });
@ -260,7 +314,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
); );
if (lastCandidatesFromSender) { if (lastCandidatesFromSender) {
console.warn('found sender ice candicate message already sent. Using it'); window.log.info('found sender ice candicate message already sent. Using it');
for (let index = 0; index < lastCandidatesFromSender.sdps.length; index++) { for (let index = 0; index < lastCandidatesFromSender.sdps.length; index++) {
const sdp = lastCandidatesFromSender.sdps[index]; const sdp = lastCandidatesFromSender.sdps[index];
const sdpMLineIndex = lastCandidatesFromSender.sdpMLineIndexes[index]; const sdpMLineIndex = lastCandidatesFromSender.sdpMLineIndexes[index];
@ -272,8 +326,6 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
window.log.info('sending ANSWER MESSAGE'); window.log.info('sending ANSWER MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), callAnswerMessage); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), callAnswerMessage);
window.inboxStore?.dispatch(answerCall({ sender: fromSender, sdps }));
} }
// tslint:disable-next-line: function-name // tslint:disable-next-line: function-name
@ -284,7 +336,7 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) {
}); });
callCache.delete(fromSender); callCache.delete(fromSender);
window.inboxStore?.dispatch(endCall({ sender: fromSender })); window.inboxStore?.dispatch(endCall({ pubkey: fromSender }));
window.log.info('sending END_CALL MESSAGE'); window.log.info('sending END_CALL MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage);
@ -292,9 +344,12 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) {
export function handleEndCallMessage(sender: string) { export function handleEndCallMessage(sender: string) {
callCache.delete(sender); callCache.delete(sender);
if (videoEventsListener) {
videoEventsListener(null, null);
}
// //
// FIXME audric trigger UI cleanup // FIXME audric trigger UI cleanup
window.inboxStore?.dispatch(endCall({ sender })); window.inboxStore?.dispatch(endCall({ pubkey: sender }));
} }
export async function handleOfferCallMessage( export async function handleOfferCallMessage(
@ -305,21 +360,31 @@ export async function handleOfferCallMessage(
console.warn({ callMessage }); console.warn({ callMessage });
const readyForOffer = const readyForOffer =
!makingOffer && (peerConnection?.signalingState == 'stable' || isSettingRemoteAnswerPending); !makingOffer && (peerConnection?.signalingState == 'stable' || isSettingRemoteAnswerPending);
// const offerCollision =
// callMessage.type === SignalService.CallMessage.Type.OFFER ||
// makingOffer ||
// peerConnection?.signalingState != 'stable';
// TODO: How should politeness be decided between client / recipient? // TODO: How should politeness be decided between client / recipient?
// ignoreOffer = !polite && offerCollision;
ignoreOffer = !true && !readyForOffer; ignoreOffer = !true && !readyForOffer;
if (ignoreOffer) { if (ignoreOffer) {
window.log?.warn('Received offer when unready for offer; Ignoring offer.'); // window.log?.warn('Received offer when unready for offer; Ignoring offer.');
console.warn('Received offer when unready for offer; Ignoring offer.');
return; return;
} }
// const description = await peerConnection?.createOffer({
// const description = await peerConnection?.createOffer({
// offerToReceiveVideo: true,
// offerToReceiveAudio: true,
// })
// @ts-ignore
await peerConnection?.setLocalDescription(); await peerConnection?.setLocalDescription();
// send via our signalling with the sdp of our pc.localDescription console.warn(peerConnection?.localDescription);
const message = new CallMessage({
type: SignalService.CallMessage.Type.ANSWER,
timestamp: Date.now(),
});
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), message);
// TODO: send via our signalling with the sdp of our pc.localDescription
} catch (err) { } catch (err) {
window.log?.error(`Error handling offer message ${err}`); window.log?.error(`Error handling offer message ${err}`);
} }
@ -328,7 +393,7 @@ export async function handleOfferCallMessage(
callCache.set(sender, new Array()); callCache.set(sender, new Array());
} }
callCache.get(sender)?.push(callMessage); callCache.get(sender)?.push(callMessage);
window.inboxStore?.dispatch(incomingCall({ sender })); window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
} }
export async function handleCallAnsweredMessage( export async function handleCallAnsweredMessage(
@ -344,7 +409,7 @@ export async function handleCallAnsweredMessage(
} }
callCache.get(sender)?.push(callMessage); callCache.get(sender)?.push(callMessage);
window.inboxStore?.dispatch(incomingCall({ sender })); window.inboxStore?.dispatch(answerCall({ pubkey: sender }));
const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] }); const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] });
if (peerConnection) { if (peerConnection) {
console.warn('Setting remote answer pending'); console.warn('Setting remote answer pending');
@ -369,7 +434,7 @@ export async function handleIceCandidatesMessage(
} }
callCache.get(sender)?.push(callMessage); callCache.get(sender)?.push(callMessage);
window.inboxStore?.dispatch(incomingCall({ sender })); // window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
if (peerConnection) { if (peerConnection) {
// tslint:disable-next-line: prefer-for-of // tslint:disable-next-line: prefer-for-of
for (let index = 0; index < callMessage.sdps.length; index++) { for (let index = 0; index < callMessage.sdps.length; index++) {

@ -3,6 +3,7 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data'; import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data';
import { import {
CallState,
ConversationNotificationSettingType, ConversationNotificationSettingType,
ConversationTypeEnum, ConversationTypeEnum,
} from '../../models/conversation'; } from '../../models/conversation';
@ -243,6 +244,7 @@ export interface ReduxConversationType {
currentNotificationSetting?: ConversationNotificationSettingType; currentNotificationSetting?: ConversationNotificationSettingType;
isPinned?: boolean; isPinned?: boolean;
callState?: CallState;
} }
export interface NotificationForConvoOption { export interface NotificationForConvoOption {
@ -747,6 +749,98 @@ const conversationsSlice = createSlice({
state.mentionMembers = action.payload; state.mentionMembers = action.payload;
return state; return state;
}, },
incomingCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (existingCallState !== undefined && existingCallState !== 'none') {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'incoming';
void foundConvo.commit();
return state;
},
endCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (!existingCallState || existingCallState === 'none') {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'none';
void foundConvo.commit();
return state;
},
answerCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (!existingCallState || existingCallState !== 'incoming') {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'connecting';
void foundConvo.commit();
return state;
},
callConnected(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (!existingCallState || existingCallState === 'ongoing') {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'ongoing';
void foundConvo.commit();
return state;
},
startingCallWith(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (existingCallState && existingCallState !== 'none') {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'offering';
void foundConvo.commit();
return state;
},
}, },
extraReducers: (builder: any) => { extraReducers: (builder: any) => {
// Add reducers for additional action types here, and handle loading state as needed // Add reducers for additional action types here, and handle loading state as needed
@ -806,6 +900,12 @@ export const {
quotedMessageToAnimate, quotedMessageToAnimate,
setNextMessageToPlayId, setNextMessageToPlayId,
updateMentionsMembers, updateMentionsMembers,
// calls
incomingCall,
endCall,
answerCall,
callConnected,
startingCallWith,
} = actions; } = actions;
export async function openConversationWithMessages(args: { export async function openConversationWithMessages(args: {

@ -66,6 +66,47 @@ export const getSelectedConversation = createSelector(
} }
); );
export const getHasIncomingCallFrom = createSelector(
getConversations,
(state: ConversationsStateType): ReduxConversationType | undefined => {
const foundEntry = Object.entries(state.conversationLookup).find(
([_convoKey, convo]) => convo.callState === 'incoming'
);
if (!foundEntry) {
return undefined;
}
return foundEntry[1];
}
);
export const getHasOngoingCallWith = createSelector(
getConversations,
(state: ConversationsStateType): ReduxConversationType | undefined => {
const foundEntry = Object.entries(state.conversationLookup).find(
([_convoKey, convo]) =>
convo.callState === 'connecting' ||
convo.callState === 'offering' ||
convo.callState === 'ongoing'
);
if (!foundEntry) {
return undefined;
}
return foundEntry[1];
}
);
export const getHasIncomingCall = createSelector(
getHasIncomingCallFrom,
(withConvo: ReduxConversationType | undefined): boolean => !!withConvo
);
export const getHasOngoingCall = createSelector(
getHasOngoingCallWith,
(withConvo: ReduxConversationType | undefined): boolean => !!withConvo
);
/** /**
* Returns true if the current conversation selected is a group conversation. * Returns true if the current conversation selected is a group conversation.
* Returns false if the current conversation selected is not a group conversation, or none are selected * Returns false if the current conversation selected is not a group conversation, or none are selected

@ -7237,6 +7237,14 @@ react-dom@^17.0.2:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.2" scheduler "^0.20.2"
react-draggable@^4.4.4:
version "4.4.4"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f"
integrity sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA==
dependencies:
clsx "^1.1.1"
prop-types "^15.6.0"
react-emoji-render@^1.2.4: react-emoji-render@^1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/react-emoji-render/-/react-emoji-render-1.2.4.tgz#fa3542a692e1eed3236f0f12b8e3a61b2818e2c2" resolved "https://registry.yarnpkg.com/react-emoji-render/-/react-emoji-render-1.2.4.tgz#fa3542a692e1eed3236f0f12b8e3a61b2818e2c2"

Loading…
Cancel
Save