make call UI react to incoming and ongoing calls

pull/1939/head
Audric Ackermann 4 years ago
parent a1601b039e
commit 8b611a2867
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -1,8 +1,14 @@
import React, { useState } from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
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';
@ -59,97 +65,62 @@ const CallWindowControls = styled.div`
transform: translateY(-100%); transform: translateY(-100%);
`; `;
// type WindowPositionType = { // TODO:
// top: string; /**
// left: string;
// } | null;
type CallStateType = 'connecting' | 'ongoing' | 'incoming' | null;
const fakeCaller = '054774a456f15c7aca42fe8d245983549000311aaebcf58ce246250c41fe227676';
export const CallContainer = () => {
const conversations = getConversationController().getConversations();
// TODO:
/**
* Add mute input, deafen, end call, possibly add person to call * Add mute input, deafen, end call, possibly add person to call
* duration - look at how duration calculated for recording. * duration - look at how duration calculated for recording.
*/ */
export const CallContainer = () => {
const hasIncomingCall = useSelector(getHasIncomingCall);
const incomingCallProps = useSelector(getHasIncomingCallFrom);
const ongoingCallProps = useSelector(getHasOngoingCallWith);
const hasOngoingCall = useSelector(getHasOngoingCall);
const [connectionState, setConnectionState] = useState<CallStateType>('incoming'); const ongoingOrIncomingPubkey = ongoingCallProps?.id || incomingCallProps?.id;
// const [callWindowPosition, setCallWindowPosition] = useState<WindowPositionType>(null)
// picking a conversation at random to test with
const foundConvo = conversations.find(convo => convo.id === fakeCaller);
if (!foundConvo) {
throw new Error('fakeconvo not found');
}
foundConvo.callState = 'incoming';
console.warn('foundConvo: ', foundConvo);
const firstCallingConvo = _.first(conversations.filter(convo => convo.callState !== undefined));
//#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);
}
}; };
const handleMouseDown = () => {
// reposition call window
};
//#endregion //#endregion
if (!hasOngoingCall && !hasIncomingCall) {
return null;
}
if (hasOngoingCall && ongoingCallProps) {
return ( return (
<> <CallWindow>
{connectionState === 'connecting' ? 'connecting...' : null}
{connectionState === 'ongoing' ? (
<CallWindow onMouseDown={handleMouseDown}>
<CallWindowInner> <CallWindowInner>
<CallWindowHeader> <CallWindowHeader>{ongoingCallProps.name}</CallWindowHeader>
{firstCallingConvo ? firstCallingConvo.getName() : 'Group name not found'}
</CallWindowHeader>
<VideoContainer /> <VideoContainer />
<CallWindowControls> <CallWindowControls>
<SessionButton text={'end call'} onClick={handleEndCall} /> <SessionButton text={'end call'} onClick={handleEndCall} />
<SessionButton text={'end call'} onClick={handleEndCall} />
</CallWindowControls> </CallWindowControls>
</CallWindowInner> </CallWindowInner>
</CallWindow> </CallWindow>
) : null} );
}
{!connectionState ? (
<SessionWrapperModal title={'incoming call'}>'none'</SessionWrapperModal>
) : null}
{connectionState === 'incoming' ? ( if (hasIncomingCall) {
return (
<SessionWrapperModal title={'incoming call'}> <SessionWrapperModal title={'incoming call'}>
<div className="session-modal__button-group"> <div className="session-modal__button-group">
<SessionButton text={'decline'} onClick={handleDeclineIncomingCall} /> <SessionButton text={'decline'} onClick={handleDeclineIncomingCall} />
@ -160,7 +131,8 @@ export const CallContainer = () => {
/> />
</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'}>
{callOptions.map(item => (
<Item <Item
key={item.value}
onClick={async () => { onClick={async () => {
// TODO: either pass param to callRecipient or call different call methods based on item selected. // TODO: either pass param to callRecipient or call different call methods based on item selected.
const convo = await getConversationById(conversationId); const convo = getConversationController().get(conversationId);
if (convo) { if (convo) {
// window?.libsession?.Utils.CallManager.USER_callRecipient(
// '054774a456f15c7aca42fe8d245983549000311aaebcf58ce246250c41fe227676'
// );
window?.libsession?.Utils.CallManager.USER_callRecipient(convo.id);
convo.callState = 'connecting'; convo.callState = 'connecting';
await convo.commit();
await CallManager.USER_callRecipient(convo.id);
} }
}} }}
> >
{item.name} {'video call'}
</Item> </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,26 +1,11 @@
import _ from 'lodash'; import _ from 'lodash';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { answerCall, callConnected, endCall, incomingCall } 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 }) => {
return { type: 'incomingCall', payload: sender };
};
const endCall = ({ sender }: { sender: string }) => {
return { type: 'endCall', payload: sender };
};
const answerCall = ({ sender, sdps }: { sender: string; sdps: Array<string> }) => {
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.
*/ */
@ -28,7 +13,7 @@ const callCache = new Map<string, Array<SignalService.CallMessage>>();
let peerConnection: RTCPeerConnection | null; let peerConnection: RTCPeerConnection | null;
const ENABLE_VIDEO = true; const ENABLE_VIDEO = false;
const configuration = { const configuration = {
configuration: { configuration: {
@ -57,13 +42,12 @@ export async function USER_callRecipient(recipient: string) {
const mediaDevices = await openMediaDevices(); const mediaDevices = await openMediaDevices();
mediaDevices.getTracks().map(track => { mediaDevices.getTracks().map(track => {
window.log.info('USER_callRecipient adding track: ', track);
peerConnection?.addTrack(track, mediaDevices); peerConnection?.addTrack(track, mediaDevices);
}); });
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 }));
} }
}); });
@ -180,27 +164,32 @@ 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 => { peerConnection.addEventListener('icecandidateerror', event => {
console.warn('icecandidateerror:', event); console.warn('icecandidateerror:', event);
}); });
peerConnection.addEventListener('negotiationneeded', async event => { peerConnection.addEventListener('negotiationneeded', event => {
console.warn('negotiationneeded:', event); console.warn('negotiationneeded:', event);
}); });
peerConnection.addEventListener('signalingstatechange', event => { peerConnection.addEventListener('signalingstatechange', _event => {
console.warn('signalingstatechange:', event); // console.warn('signalingstatechange:', event);
}); });
peerConnection.addEventListener('ontrack', event => { peerConnection.addEventListener('ontrack', event => {
console.warn('ontrack:', event); console.warn('ontrack:', event);
}); });
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 }));
} }
}); });
@ -237,7 +226,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];
@ -250,7 +239,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), callAnswerMessage); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), callAnswerMessage);
window.inboxStore?.dispatch(answerCall({ sender: fromSender, sdps })); window.inboxStore?.dispatch(answerCall({ pubkey: fromSender }));
} }
// tslint:disable-next-line: function-name // tslint:disable-next-line: function-name
@ -261,7 +250,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);
@ -271,18 +260,15 @@ export function handleEndCallMessage(sender: string) {
callCache.delete(sender); callCache.delete(sender);
// //
// 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 function handleOfferCallMessage(sender: string, callMessage: SignalService.CallMessage) {
sender: string,
callMessage: SignalService.CallMessage
) {
if (!callCache.has(sender)) { if (!callCache.has(sender)) {
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(
@ -298,7 +284,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) {
await peerConnection.setRemoteDescription(remoteDesc); await peerConnection.setRemoteDescription(remoteDesc);
@ -320,7 +306,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,81 @@ 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;
},
}, },
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 +883,11 @@ export const {
quotedMessageToAnimate, quotedMessageToAnimate,
setNextMessageToPlayId, setNextMessageToPlayId,
updateMentionsMembers, updateMentionsMembers,
// calls
incomingCall,
endCall,
answerCall,
callConnected,
} = 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

Loading…
Cancel
Save