You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
564 lines
20 KiB
564 lines
20 KiB
import type {
|
|
IVoipRoom,
|
|
IUser,
|
|
VoipEventDataSignature,
|
|
ICallerInfo,
|
|
ICallDetails,
|
|
ILivechatVisitor,
|
|
Serialized,
|
|
} from '@rocket.chat/core-typings';
|
|
import {
|
|
VoipClientEvents,
|
|
isVoipEventAgentCalled,
|
|
isVoipEventAgentConnected,
|
|
isVoipEventCallerJoined,
|
|
isVoipEventQueueMemberAdded,
|
|
isVoipEventQueueMemberRemoved,
|
|
isVoipEventCallAbandoned,
|
|
UserState,
|
|
} from '@rocket.chat/core-typings';
|
|
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
|
|
import { Random } from '@rocket.chat/random';
|
|
import type { Device, IExperimentalHTMLAudioElement } from '@rocket.chat/ui-contexts';
|
|
import {
|
|
useRoute,
|
|
useUser,
|
|
useSetting,
|
|
useEndpoint,
|
|
useStream,
|
|
useSetOutputMediaDevice,
|
|
useSetInputMediaDevice,
|
|
useSetModal,
|
|
useTranslation,
|
|
} from '@rocket.chat/ui-contexts';
|
|
// import { useRoute, useUser, useSetting, useEndpoint, useStream, useSetModal } from '@rocket.chat/ui-contexts';
|
|
import type { FC } from 'react';
|
|
import React, { useMemo, useRef, useCallback, useEffect, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import type { OutgoingByeRequest } from 'sip.js/lib/core';
|
|
|
|
import { CustomSounds } from '../../../app/custom-sounds/client';
|
|
import { getUserPreference } from '../../../app/utils/client';
|
|
import { isOutboundClient, useVoipClient } from '../../../ee/client/hooks/useVoipClient';
|
|
import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber';
|
|
import { WrapUpCallModal } from '../../../ee/client/voip/components/modals/WrapUpCallModal';
|
|
import type { CallContextValue } from '../../contexts/CallContext';
|
|
import { CallContext, useIsVoipEnterprise } from '../../contexts/CallContext';
|
|
import { useDialModal } from '../../hooks/useDialModal';
|
|
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
|
|
import type { QueueAggregator } from '../../lib/voip/QueueAggregator';
|
|
|
|
type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended';
|
|
|
|
const startRingback = (user: IUser, soundId: VoipSound, loop = true): void => {
|
|
const audioVolume = getUserPreference(user, 'notificationsSoundVolume', 100) as number;
|
|
CustomSounds.play(soundId, {
|
|
volume: Number((audioVolume / 100).toPrecision(2)),
|
|
loop,
|
|
});
|
|
};
|
|
|
|
const stopRingBackById = (soundId: VoipSound): void => {
|
|
const sound = CustomSounds.getSound(soundId);
|
|
CustomSounds.pause(soundId);
|
|
CustomSounds.remove(sound);
|
|
};
|
|
|
|
const stopTelephoneRingback = (): void => stopRingBackById('telephone');
|
|
const stopOutboundCallRinging = (): void => stopRingBackById('outbound-call-ringing');
|
|
|
|
const stopAllRingback = (): void => {
|
|
stopTelephoneRingback();
|
|
stopOutboundCallRinging();
|
|
};
|
|
|
|
type NetworkState = 'online' | 'offline';
|
|
|
|
export const CallProvider: FC = ({ children }) => {
|
|
const [clientState, setClientState] = useState<'registered' | 'unregistered'>('unregistered');
|
|
|
|
const voipEnabled = useSetting('VoIP_Enabled');
|
|
const subscribeToNotifyUser = useStream('notify-user');
|
|
const dispatchEvent = useEndpoint('POST', '/v1/voip/events');
|
|
const visitorEndpoint = useEndpoint('POST', '/v1/livechat/visitor');
|
|
const voipEndpoint = useEndpoint('GET', '/v1/voip/room');
|
|
const voipCloseRoomEndpoint = useEndpoint('POST', '/v1/voip/room.close');
|
|
const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search');
|
|
const setModal = useSetModal();
|
|
const t = useTranslation();
|
|
|
|
const result = useVoipClient();
|
|
const user = useUser();
|
|
const homeRoute = useRoute('home');
|
|
const setOutputMediaDevice = useSetOutputMediaDevice();
|
|
const setInputMediaDevice = useSetInputMediaDevice();
|
|
|
|
const hasVoIPEnterpriseLicense = useIsVoipEnterprise();
|
|
|
|
const remoteAudioMediaRef = useRef<IExperimentalHTMLAudioElement>(null); // TODO: Create a dedicated file for the AUDIO and make the controls accessible
|
|
|
|
const [queueCounter, setQueueCounter] = useState(0);
|
|
const [queueName, setQueueName] = useState('');
|
|
const [roomInfo, setRoomInfo] = useState<{ v: { token?: string }; rid: string }>({ v: {}, rid: '' });
|
|
|
|
const { openDialModal } = useDialModal();
|
|
|
|
const closeRoom = useCallback(
|
|
async (data = {}): Promise<void> => {
|
|
roomInfo &&
|
|
(await voipCloseRoomEndpoint({
|
|
rid: roomInfo.rid,
|
|
token: roomInfo.v.token || '',
|
|
options: { comment: data?.comment, tags: data?.tags },
|
|
}));
|
|
homeRoute.push({});
|
|
|
|
const queueAggregator = result.voipClient?.getAggregator();
|
|
if (queueAggregator) {
|
|
queueAggregator.callEnded();
|
|
}
|
|
},
|
|
[homeRoute, result?.voipClient, roomInfo, voipCloseRoomEndpoint],
|
|
);
|
|
|
|
const openWrapUpModal = useCallback((): void => {
|
|
setModal(() => <WrapUpCallModal closeRoom={closeRoom} />);
|
|
}, [closeRoom, setModal]);
|
|
|
|
const changeAudioOutputDevice = useMutableCallback((selectedAudioDevice: Device): void => {
|
|
remoteAudioMediaRef?.current &&
|
|
setOutputMediaDevice({ outputDevice: selectedAudioDevice, HTMLAudioElement: remoteAudioMediaRef.current });
|
|
});
|
|
|
|
const changeAudioInputDevice = useMutableCallback((selectedAudioDevice: Device): void => {
|
|
if (!result.voipClient) {
|
|
return;
|
|
}
|
|
const constraints = { audio: { deviceId: { exact: selectedAudioDevice.id } } };
|
|
|
|
// TODO: Migrate the classes that manage MediaStream to a more react based approach (using contexts/providers perhaps)
|
|
// For now the MediaStream management is very coupled with the VoIP client,
|
|
// decoupling it will make it usable by other areas of the project that needs to handle MediaStreams and avoid code duplication
|
|
result.voipClient.changeAudioInputDevice(constraints);
|
|
|
|
setInputMediaDevice(selectedAudioDevice);
|
|
});
|
|
|
|
const [queueAggregator, setQueueAggregator] = useState<QueueAggregator>();
|
|
|
|
const [networkStatus, setNetworkStatus] = useState<NetworkState>('online');
|
|
|
|
useEffect(() => {
|
|
const { voipClient } = result || {};
|
|
|
|
if (!voipClient) {
|
|
return;
|
|
}
|
|
|
|
setQueueAggregator(voipClient.getAggregator());
|
|
|
|
return (): void => {
|
|
if (clientState === 'registered') {
|
|
return voipClient.unregister();
|
|
}
|
|
};
|
|
}, [result, clientState]);
|
|
|
|
const openRoom = useCallback((rid: IVoipRoom['_id']): void => {
|
|
roomCoordinator.openRouteLink('v', { rid });
|
|
}, []);
|
|
|
|
const findOrCreateVisitor = useCallback(
|
|
async (caller: ICallerInfo): Promise<Serialized<ILivechatVisitor>> => {
|
|
const phone = parseOutboundPhoneNumber(caller.callerId);
|
|
|
|
const { contact } = await getContactBy({ phone });
|
|
|
|
if (contact) {
|
|
return contact;
|
|
}
|
|
|
|
const { visitor } = await visitorEndpoint({
|
|
visitor: {
|
|
token: Random.id(),
|
|
phone,
|
|
name: caller.callerName || phone,
|
|
},
|
|
});
|
|
|
|
return visitor;
|
|
},
|
|
[getContactBy, visitorEndpoint],
|
|
);
|
|
|
|
const createRoom = useCallback(
|
|
async (caller: ICallerInfo, direction: IVoipRoom['direction'] = 'inbound'): Promise<IVoipRoom['_id']> => {
|
|
if (!user) {
|
|
return '';
|
|
}
|
|
try {
|
|
const visitor = await findOrCreateVisitor(caller);
|
|
const voipRoom = await voipEndpoint({ token: visitor.token, agentId: user._id, direction });
|
|
openRoom(voipRoom.room._id);
|
|
voipRoom.room && setRoomInfo({ v: { token: voipRoom.room.v.token }, rid: voipRoom.room._id });
|
|
const queueAggregator = result.voipClient?.getAggregator();
|
|
if (queueAggregator) {
|
|
queueAggregator.callStarted();
|
|
}
|
|
return voipRoom.room._id;
|
|
} catch (error) {
|
|
console.error(`Error while creating a visitor ${error}`);
|
|
return '';
|
|
}
|
|
},
|
|
[openRoom, result.voipClient, user, voipEndpoint, findOrCreateVisitor],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!voipEnabled || !user || !queueAggregator) {
|
|
return;
|
|
}
|
|
|
|
const handleEventReceived = async (event: VoipEventDataSignature): Promise<void> => {
|
|
if (isVoipEventAgentCalled(event)) {
|
|
const { data } = event;
|
|
queueAggregator.callRinging({ queuename: data.queue, callerid: data.callerId });
|
|
setQueueName(queueAggregator.getCurrentQueueName());
|
|
return;
|
|
}
|
|
if (isVoipEventAgentConnected(event)) {
|
|
const { data } = event;
|
|
queueAggregator.callPickedup({ queuename: data.queue, queuedcalls: data.queuedCalls, waittimeinqueue: data.waitTimeInQueue });
|
|
setQueueName(queueAggregator.getCurrentQueueName());
|
|
setQueueCounter(queueAggregator.getCallWaitingCount());
|
|
return;
|
|
}
|
|
if (isVoipEventCallerJoined(event)) {
|
|
const { data } = event;
|
|
queueAggregator.queueJoined({ queuename: data.queue, callerid: data.callerId, queuedcalls: data.queuedCalls });
|
|
setQueueCounter(queueAggregator.getCallWaitingCount());
|
|
return;
|
|
}
|
|
if (isVoipEventQueueMemberAdded(event)) {
|
|
const { data } = event;
|
|
queueAggregator.memberAdded({ queuename: data.queue, queuedcalls: data.queuedCalls });
|
|
setQueueName(queueAggregator.getCurrentQueueName());
|
|
setQueueCounter(queueAggregator.getCallWaitingCount());
|
|
return;
|
|
}
|
|
if (isVoipEventQueueMemberRemoved(event)) {
|
|
const { data } = event;
|
|
queueAggregator.memberRemoved({ queuename: data.queue, queuedcalls: data.queuedCalls });
|
|
setQueueCounter(queueAggregator.getCallWaitingCount());
|
|
return;
|
|
}
|
|
if (isVoipEventCallAbandoned(event)) {
|
|
const { data } = event;
|
|
queueAggregator.queueAbandoned({ queuename: data.queue, queuedcallafterabandon: data.queuedCallAfterAbandon });
|
|
setQueueName(queueAggregator.getCurrentQueueName());
|
|
setQueueCounter(queueAggregator.getCallWaitingCount());
|
|
return;
|
|
}
|
|
|
|
console.warn('Unknown event received');
|
|
};
|
|
|
|
return subscribeToNotifyUser(`${user._id}/voip.events`, handleEventReceived);
|
|
}, [subscribeToNotifyUser, user, queueAggregator, voipEnabled]);
|
|
|
|
// This was causing event duplication before, so we'll leave this here for now
|
|
useEffect(() => {
|
|
if (!voipEnabled || !user || !queueAggregator) {
|
|
return;
|
|
}
|
|
|
|
return subscribeToNotifyUser(`${user._id}/call.hangup`, (event): void => {
|
|
setQueueName(queueAggregator.getCurrentQueueName());
|
|
|
|
if (hasVoIPEnterpriseLicense) {
|
|
openWrapUpModal();
|
|
return;
|
|
}
|
|
|
|
closeRoom();
|
|
|
|
dispatchEvent({ event: VoipClientEvents['VOIP-CALL-ENDED'], rid: event.roomId });
|
|
});
|
|
}, [openWrapUpModal, queueAggregator, subscribeToNotifyUser, user, voipEnabled, dispatchEvent, hasVoIPEnterpriseLicense, closeRoom]);
|
|
|
|
useEffect(() => {
|
|
if (!result.voipClient) {
|
|
return;
|
|
}
|
|
|
|
const offRegistered = result.voipClient.on('registered', (): void => setClientState('registered'));
|
|
const offUnregistered = result.voipClient.on('unregistered', (): void => setClientState('unregistered'));
|
|
|
|
return (): void => {
|
|
offRegistered();
|
|
offUnregistered();
|
|
};
|
|
}, [result.voipClient]);
|
|
|
|
useEffect(() => {
|
|
if (!result.voipClient) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* This code may need a revisit when we handle callinqueue differently.
|
|
* Check clickup tasks for more details
|
|
* https://app.clickup.com/t/22hy1k4
|
|
* When customer called a queue (Either using skype or using internal number), call would get established
|
|
* customer would hear agent's voice but agent would not hear anything from customer.
|
|
* This issue was observed on unstable. It was found to be inconsistent to reproduce.
|
|
* On some developer env, it would happen randomly. On Safari it did not happen if
|
|
* user refreshes before taking every call.
|
|
*
|
|
* The reason behind this was as soon as agent accepts a call, queueCounter would change.
|
|
* This change will trigger re-rendering of media and creation of audio element.
|
|
* This audio element gets used by voipClient to render the remote audio.
|
|
* Because the re-render happened, it would hold a stale reference.
|
|
*
|
|
* If the dom is inspected, audio element just before body is usually created by this class.
|
|
* this audio element.srcObject contains null value. In working case, it should display
|
|
* valid stream object.
|
|
*
|
|
* Reason for inconsistencies :
|
|
* This element is utilized in VoIPUser::setupRemoteMedia
|
|
* This function is called when webRTC receives a remote track event. i.e when the webrtc's peer connection
|
|
* starts receiving media. This event call back depends on several factors. How does asterisk setup streams.
|
|
* How does it creates a bridge which patches up the agent and customer (Media is flowing thru asterisk).
|
|
* When it works in de-environment, it was observed that the audio element in dom and the audio element hold
|
|
* by VoIPUser is different. Nonetheless, this stale audio element holds valid media stream, which is being played.
|
|
* Hence sometimes the audio is heard.
|
|
*
|
|
* Ideally call component once gets stable, should not get rerendered. Queue, Room creation are the parameters
|
|
* which should be independent and should not control the call component.
|
|
*
|
|
* Solution :
|
|
* Either make the audio elemenent rendered independent of rest of the DOM.
|
|
* or implement useEffect. This useEffect will reset the rendering elements with the latest audio tag.
|
|
*
|
|
* Note : If this code gets refactor, revisit the line below to check if this call is needed.
|
|
*
|
|
*/
|
|
remoteAudioMediaRef.current && result.voipClient.switchMediaRenderer({ remoteMediaElement: remoteAudioMediaRef.current });
|
|
}, [result.voipClient]);
|
|
|
|
useEffect(() => {
|
|
if (!result.voipClient) {
|
|
return;
|
|
}
|
|
|
|
if (!user) {
|
|
return;
|
|
}
|
|
|
|
const onCallEstablished = async (callDetails: ICallDetails): Promise<undefined> => {
|
|
if (!callDetails.callInfo) {
|
|
return;
|
|
}
|
|
stopAllRingback();
|
|
if (callDetails.userState !== UserState.UAC) {
|
|
return;
|
|
}
|
|
// Agent has sent Invite. So it must create a room.
|
|
const { callInfo } = callDetails;
|
|
// While making the call, there is no remote media element available.
|
|
// When the call is ringing we have that element created. But we still
|
|
// do not want it to be attached.
|
|
// When call gets established, then switch the media renderer.
|
|
remoteAudioMediaRef.current && result.voipClient?.switchMediaRenderer({ remoteMediaElement: remoteAudioMediaRef.current });
|
|
const roomId = await createRoom(callInfo, 'outbound');
|
|
dispatchEvent({ event: VoipClientEvents['VOIP-CALL-STARTED'], rid: roomId });
|
|
};
|
|
|
|
const onNetworkConnected = (): void => {
|
|
if (networkStatus === 'offline') {
|
|
setNetworkStatus('online');
|
|
}
|
|
};
|
|
|
|
const onNetworkDisconnected = (): void => {
|
|
// Transitioning from online -> offline
|
|
// If there is ongoing call, terminate it or if we are processing an incoming/outgoing call
|
|
// reject it.
|
|
if (networkStatus === 'online') {
|
|
setNetworkStatus('offline');
|
|
switch (result.voipClient?.callerInfo.state) {
|
|
case 'IN_CALL':
|
|
case 'ON_HOLD':
|
|
result.voipClient?.endCall();
|
|
break;
|
|
case 'OFFER_RECEIVED':
|
|
case 'ANSWER_SENT':
|
|
result.voipClient?.rejectCall();
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const onRinging = (): void => {
|
|
startRingback(user, 'outbound-call-ringing');
|
|
};
|
|
|
|
const onIncomingCallRinging = (): void => {
|
|
startRingback(user, 'telephone');
|
|
};
|
|
|
|
const onCallTerminated = (): void => {
|
|
startRingback(user, 'call-ended', false);
|
|
stopAllRingback();
|
|
};
|
|
|
|
const onCallFailed = (reason: 'Not Found' | 'Address Incomplete' | 'Request Terminated' | string): void => {
|
|
switch (reason) {
|
|
case 'Not Found':
|
|
// This happens when the call matches dialplan and goes to the world, but the trunk doesnt find the number.
|
|
openDialModal({ errorMessage: t('Dialed_number_doesnt_exist') });
|
|
break;
|
|
case 'Address Incomplete':
|
|
// This happens when the dialed number doesnt match a valid asterisk dialplan pattern or the number is invalid.
|
|
openDialModal({ errorMessage: t('Dialed_number_is_incomplete') });
|
|
break;
|
|
case 'Request Terminated':
|
|
break;
|
|
default:
|
|
openDialModal({ errorMessage: t('Something_went_wrong_try_again_later') });
|
|
}
|
|
};
|
|
|
|
result.voipClient.onNetworkEvent('connected', onNetworkConnected);
|
|
result.voipClient.onNetworkEvent('disconnected', onNetworkDisconnected);
|
|
result.voipClient.onNetworkEvent('connectionerror', onNetworkDisconnected);
|
|
result.voipClient.onNetworkEvent('localnetworkonline', onNetworkConnected);
|
|
result.voipClient.onNetworkEvent('localnetworkoffline', onNetworkDisconnected);
|
|
result.voipClient.on('callestablished', onCallEstablished);
|
|
result.voipClient.on('ringing', onRinging); // not called for incoming call
|
|
result.voipClient.on('incomingcall', onIncomingCallRinging);
|
|
result.voipClient.on('callterminated', onCallTerminated);
|
|
|
|
if (isOutboundClient(result.voipClient)) {
|
|
result.voipClient.on('callfailed', onCallFailed);
|
|
}
|
|
|
|
return (): void => {
|
|
result.voipClient?.offNetworkEvent('connected', onNetworkConnected);
|
|
result.voipClient?.offNetworkEvent('disconnected', onNetworkDisconnected);
|
|
result.voipClient?.offNetworkEvent('connectionerror', onNetworkDisconnected);
|
|
result.voipClient?.offNetworkEvent('localnetworkonline', onNetworkConnected);
|
|
result.voipClient?.offNetworkEvent('localnetworkoffline', onNetworkDisconnected);
|
|
result.voipClient?.off('incomingcall', onIncomingCallRinging);
|
|
result.voipClient?.off('ringing', onRinging);
|
|
result.voipClient?.off('callestablished', onCallEstablished);
|
|
result.voipClient?.off('callterminated', onCallTerminated);
|
|
|
|
if (isOutboundClient(result.voipClient)) {
|
|
result.voipClient?.off('callfailed', onCallFailed);
|
|
}
|
|
};
|
|
}, [createRoom, dispatchEvent, networkStatus, openDialModal, result.voipClient, t, user]);
|
|
|
|
const contextValue: CallContextValue = useMemo(() => {
|
|
if (!voipEnabled) {
|
|
return {
|
|
enabled: false,
|
|
ready: false,
|
|
outBoundCallsAllowed: undefined, // set to true only if enterprise license is present.
|
|
outBoundCallsEnabled: undefined, // set to true even if enterprise license is not present.
|
|
outBoundCallsEnabledForUser: undefined, // set to true if the user has enterprise license, but is not able to make outbound calls. (busy, or disabled)
|
|
};
|
|
}
|
|
|
|
if (!user?.extension) {
|
|
return {
|
|
enabled: false,
|
|
ready: false,
|
|
outBoundCallsAllowed: undefined, // set to true only if enterprise license is present.
|
|
outBoundCallsEnabled: undefined, // set to true even if enterprise license is not present.
|
|
outBoundCallsEnabledForUser: undefined, // set to true if the user has enterprise license, but is not able to make outbound calls. (busy, or disabled)
|
|
};
|
|
}
|
|
|
|
if (result.error) {
|
|
return {
|
|
enabled: true,
|
|
ready: false,
|
|
error: result.error,
|
|
outBoundCallsAllowed: undefined, // set to true only if enterprise license is present.
|
|
outBoundCallsEnabled: undefined, // set to true even if enterprise license is not present.
|
|
outBoundCallsEnabledForUser: undefined, // set to true if the user has enterprise license, but is not able to make outbound calls. (busy, or disabled)
|
|
};
|
|
}
|
|
|
|
if (!result.voipClient) {
|
|
return {
|
|
enabled: true,
|
|
ready: false,
|
|
outBoundCallsAllowed: undefined, // set to true only if enterprise license is present.
|
|
outBoundCallsEnabled: undefined, // set to true even if enterprise license is not present.
|
|
outBoundCallsEnabledForUser: undefined, // set to true if the user has enterprise license, but is not able to make outbound calls. (busy, or disabled)
|
|
};
|
|
}
|
|
|
|
const { registrationInfo, voipClient } = result;
|
|
|
|
return {
|
|
outBoundCallsAllowed: hasVoIPEnterpriseLicense, // set to true only if enterprise license is present.
|
|
outBoundCallsEnabled: hasVoIPEnterpriseLicense, // set to true even if enterprise license is not present.
|
|
outBoundCallsEnabledForUser:
|
|
hasVoIPEnterpriseLicense && clientState === 'registered' && !['IN_CALL', 'ON_HOLD'].includes(voipClient.callerInfo.state), // set to true if the user has enterprise license, but is not able to make outbound calls. (busy, or disabled)
|
|
|
|
enabled: true,
|
|
ready: true,
|
|
openedRoomInfo: roomInfo,
|
|
voipClient,
|
|
registrationInfo,
|
|
queueCounter,
|
|
queueName,
|
|
actions: {
|
|
mute: (): Promise<void> => voipClient.muteCall(true), // voipClient.mute(),
|
|
unmute: (): Promise<void> => voipClient.muteCall(false), // voipClient.unmute()
|
|
pause: (): Promise<void> => voipClient.holdCall(true), // voipClient.pause()
|
|
resume: (): Promise<void> => voipClient.holdCall(false), // voipClient.resume()
|
|
end: (): Promise<OutgoingByeRequest | void> => voipClient.endCall(),
|
|
pickUp: async (): Promise<void | null> =>
|
|
remoteAudioMediaRef.current && voipClient.acceptCall({ remoteMediaElement: remoteAudioMediaRef.current }),
|
|
reject: (): Promise<void> => voipClient.rejectCall(),
|
|
},
|
|
openRoom,
|
|
createRoom,
|
|
closeRoom,
|
|
networkStatus,
|
|
openWrapUpModal,
|
|
changeAudioOutputDevice,
|
|
changeAudioInputDevice,
|
|
register: (): void => voipClient.register(),
|
|
unregister: (): void => voipClient.unregister(),
|
|
};
|
|
}, [
|
|
voipEnabled,
|
|
user?.extension,
|
|
result,
|
|
hasVoIPEnterpriseLicense,
|
|
clientState,
|
|
roomInfo,
|
|
queueCounter,
|
|
queueName,
|
|
openRoom,
|
|
createRoom,
|
|
closeRoom,
|
|
openWrapUpModal,
|
|
changeAudioOutputDevice,
|
|
changeAudioInputDevice,
|
|
networkStatus,
|
|
]);
|
|
|
|
return (
|
|
<CallContext.Provider value={contextValue}>
|
|
{children}
|
|
{contextValue.enabled && createPortal(<audio ref={remoteAudioMediaRef} />, document.body)}
|
|
</CallContext.Provider>
|
|
);
|
|
};
|
|
|