diff --git a/apps/meteor/client/contexts/CallContext.ts b/apps/meteor/client/contexts/CallContext.ts index bb739416a74c..47ec8a25bc6d 100644 --- a/apps/meteor/client/contexts/CallContext.ts +++ b/apps/meteor/client/contexts/CallContext.ts @@ -72,6 +72,8 @@ export const useIsCallError = (): boolean => { return Boolean(isCallContextError(context)); }; +export const useCallContext = (): CallContextValue => useContext(CallContext); + export const useCallActions = (): CallActionsType => { const context = useContext(CallContext); @@ -142,6 +144,7 @@ export const useCallClient = (): VoIPUser => { if (!isCallContextReady(context)) { throw new Error('useClient only if Calls are enabled and ready'); } + return context.voipClient; }; diff --git a/apps/meteor/client/contexts/VoIPAgentContext.ts b/apps/meteor/client/contexts/VoIPAgentContext.ts new file mode 100644 index 000000000000..ef27a32e09c3 --- /dev/null +++ b/apps/meteor/client/contexts/VoIPAgentContext.ts @@ -0,0 +1,23 @@ +import { createContext, Dispatch, SetStateAction } from 'react'; + +export type VoIPAgentContextValue = { + agentEnabled: boolean; + registered: boolean; + networkStatus: 'online' | 'offline'; + voipButtonEnabled: boolean; + setAgentEnabled: Dispatch>; + setRegistered: Dispatch>; + setNetworkStatus: Dispatch>; + setVoipButtonEnabled: Dispatch>; +}; + +export const VoIPAgentContext = createContext({ + agentEnabled: false, + registered: false, + networkStatus: 'offline', + voipButtonEnabled: false, + setAgentEnabled: () => undefined, + setRegistered: () => undefined, + setNetworkStatus: () => undefined, + setVoipButtonEnabled: () => undefined, +}); diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index ba1e359e4acd..72756543a250 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -21,9 +21,10 @@ import { OutgoingByeRequest } from 'sip.js/lib/core'; import { CustomSounds } from '../../../app/custom-sounds/client'; import { getUserPreference } from '../../../app/utils/client'; import { WrapUpCallModal } from '../../components/voip/modal/WrapUpCallModal'; -import { CallContext, CallContextValue, useCallCloseRoom } from '../../contexts/CallContext'; +import { CallContext, CallContextValue } from '../../contexts/CallContext'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { QueueAggregator } from '../../lib/voip/QueueAggregator'; +import VoIPAgentProvider from '../VoIPAgentProvider'; import { useVoipClient } from './hooks/useVoipClient'; const startRingback = (user: IUser): void => { @@ -44,6 +45,9 @@ export const CallProvider: FC = ({ children }) => { 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 setModal = useSetModal(); const result = useVoipClient(); @@ -54,10 +58,29 @@ export const CallProvider: FC = ({ children }) => { const [queueCounter, setQueueCounter] = useState(0); const [queueName, setQueueName] = useState(''); + const [roomInfo, setRoomInfo] = useState<{ v: { token?: string }; rid: string }>(); + + const closeRoom = useCallback( + async (data): Promise => { + 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(() => ); - }, [setModal]); + setModal(() => ); + }, [closeRoom, setModal]); const [queueAggregator, setQueueAggregator] = useState(); @@ -234,12 +257,6 @@ export const CallProvider: FC = ({ children }) => { }; }, [onNetworkConnected, onNetworkDisconnected, result.voipClient]); - const visitorEndpoint = useEndpoint('POST', '/v1/livechat/visitor'); - const voipEndpoint = useEndpoint('GET', '/v1/voip/room'); - const voipCloseRoomEndpoint = useEndpoint('POST', '/v1/voip/room.close'); - - const [roomInfo, setRoomInfo] = useState<{ v: { token?: string }; rid: string }>(); - const openRoom = (rid: IVoipRoom['_id']): void => { roomCoordinator.openRouteLink('v', { rid }); }; @@ -319,34 +336,24 @@ export const CallProvider: FC = ({ children }) => { } return ''; }, - closeRoom: async ({ comment, tags }: { comment?: string; tags?: string[] }): Promise => { - roomInfo && (await voipCloseRoomEndpoint({ rid: roomInfo.rid, token: roomInfo.v.token || '', options: { comment, tags } })); - homeRoute.push({}); - const queueAggregator = voipClient.getAggregator(); - if (queueAggregator) { - queueAggregator.callEnded(); - } - }, + closeRoom, openWrapUpModal, }; - }, [ - voipEnabled, - user, - result, - roomInfo, - queueCounter, - queueName, - openWrapUpModal, - visitorEndpoint, - voipEndpoint, - voipCloseRoomEndpoint, - homeRoute, - ]); + }, [voipEnabled, user, result, roomInfo, queueCounter, queueName, closeRoom, openWrapUpModal, visitorEndpoint, voipEndpoint]); return ( - {children} - {contextValue.enabled && createPortal( ); }; diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index ef0e5885cf7c..56273b2f854a 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -31,15 +31,15 @@ const MeteorProvider: FC = ({ children }) => ( - - + + {children} - - + + diff --git a/apps/meteor/client/providers/VoIPAgentProvider.tsx b/apps/meteor/client/providers/VoIPAgentProvider.tsx new file mode 100644 index 000000000000..79f45cf32dfd --- /dev/null +++ b/apps/meteor/client/providers/VoIPAgentProvider.tsx @@ -0,0 +1,102 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; + +import { useCallActions, useCallClient } from '../contexts/CallContext'; +import { VoIPAgentContext } from '../contexts/VoIPAgentContext'; + +const VoIPAgentProvider: FC = ({ children }) => { + const [agentEnabled, setAgentEnabled] = useState(false); + const [registered, setRegistered] = useState(false); + const [networkStatus, setNetworkStatus] = useState<'online' | 'offline'>('online'); + const [voipButtonEnabled, setVoipButtonEnabled] = useState(false); + const callActions = useCallActions(); + + const voipClient = useCallClient(); + const registerState = useMemo(() => voipClient.getRegistrarState(), [voipClient]); + + const toggleRegistered = useCallback((): void => { + setRegistered((registered) => !registered); + }, []); + + const toggleRegistrationError = useCallback((): void => { + setRegistered(false); + setAgentEnabled(false); + }, []); + + const onNetworkConnected = useCallback((): void => { + setVoipButtonEnabled(['IN_CALL', 'ON_HOLD'].includes(voipClient.callerInfo.state)); + setNetworkStatus('online'); + }, [setNetworkStatus, setVoipButtonEnabled, voipClient.callerInfo.state]); + + const onNetworkDisconnected = useCallback((): void => { + setVoipButtonEnabled(true); + setNetworkStatus('offline'); + }, [setNetworkStatus, setVoipButtonEnabled]); + + useEffect(() => { + if (!agentEnabled) { + return; + } + + voipClient.register(); + + return (): void => voipClient.unregister(); + }, [agentEnabled, voipClient]); + + useEffect(() => { + setVoipButtonEnabled(['IN_CALL', 'ON_HOLD'].includes(voipClient.callerInfo.state)); + }, [setVoipButtonEnabled, voipClient.callerInfo.state]); + + useEffect(() => { + setRegistered(registerState === 'registered'); + }, [registerState]); + + useEffect(() => { + if (voipButtonEnabled) { + return; + } + + voipClient.callerInfo.state === 'OFFER_RECEIVED' && callActions.reject(); + }, [callActions, voipButtonEnabled, voipClient.callerInfo.state]); + + useEffect(() => { + voipClient.on('registered', toggleRegistered); + voipClient.on('unregistered', toggleRegistered); + voipClient.on('registrationerror', toggleRegistrationError); + voipClient.on('unregistrationerror', toggleRegistrationError); + voipClient.onNetworkEvent('connected', onNetworkConnected); + voipClient.onNetworkEvent('disconnected', onNetworkDisconnected); + voipClient.onNetworkEvent('connectionerror', onNetworkDisconnected); + voipClient.onNetworkEvent('localnetworkonline', onNetworkConnected); + voipClient.onNetworkEvent('localnetworkoffline', onNetworkDisconnected); + + return (): void => { + voipClient.off('registered', toggleRegistered); + voipClient.off('unregistered', toggleRegistered); + voipClient.off('registrationerror', toggleRegistrationError); + voipClient.off('unregistrationerror', toggleRegistrationError); + voipClient.offNetworkEvent('connected', onNetworkConnected); + voipClient.offNetworkEvent('disconnected', onNetworkDisconnected); + voipClient.offNetworkEvent('connectionerror', onNetworkDisconnected); + voipClient.offNetworkEvent('localnetworkonline', onNetworkConnected); + voipClient.offNetworkEvent('localnetworkoffline', onNetworkDisconnected); + }; + }, [voipClient, onNetworkConnected, onNetworkDisconnected, toggleRegistered, toggleRegistrationError]); + + return ( + + ); +}; + +export default VoIPAgentProvider; diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 6f4ca4df40f9..98e2b9474534 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -130,7 +130,6 @@ export const VoipFooter = ({ small square danger - primary onClick={(e): unknown => { e.stopPropagation(); toggleMic(false); @@ -152,7 +151,6 @@ export const VoipFooter = ({ small square success - primary onClick={async (): Promise => { callActions.pickUp(); const rid = await createRoom(caller); diff --git a/apps/meteor/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx b/apps/meteor/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx index 97b75a42ccac..08b917ea58e2 100644 --- a/apps/meteor/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx +++ b/apps/meteor/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx @@ -1,29 +1,30 @@ import { Sidebar } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { ReactElement, useEffect, useState } from 'react'; +import React, { ReactElement, useCallback } from 'react'; -import { useCallClient, useCallerInfo, useCallActions } from '../../../contexts/CallContext'; +// import { useCallActions, useCallClient } from '../../../contexts/CallContext'; +import { useVoipAgent } from '../hooks/useVoipAgent'; -type NetworkState = 'online' | 'offline'; export const OmnichannelCallToggleReady = (): ReactElement => { - const [agentEnabled, setAgentEnabled] = useState(false); // TODO: get from AgentInfo const t = useTranslation(); - const [registered, setRegistered] = useState(false); - const voipClient = useCallClient(); - const [disableButtonClick, setDisableButtonClick] = useState(false); - const [networkStatus, setNetworkStatus] = useState('online'); - const callerInfo = useCallerInfo(); - const callActions = useCallActions(); - const getTooltip = (): string => { + const { agentEnabled, networkStatus, registered, voipButtonEnabled, setAgentEnabled } = useVoipAgent(); + const onClickVoipButton = useCallback((): void => { + if (voipButtonEnabled) { + return; + } + + setAgentEnabled(!agentEnabled); + }, [agentEnabled, setAgentEnabled, voipButtonEnabled]); + + const getTitle = (): string => { if (networkStatus === 'offline') { return t('Signaling_connection_disconnected'); } - if (!registered) { + if (registered) { return t('Enable'); } - if (!disableButtonClick) { + if (!voipButtonEnabled) { // Color for this state still not defined return t('Disable'); } @@ -46,97 +47,10 @@ export const OmnichannelCallToggleReady = (): ReactElement => { }; const voipCallIcon = { - title: getTooltip(), - color: getColor(), + title: getTitle(), icon: getIcon(), - } as const; - - useEffect(() => { - // Any of the 2 states means the user is already talking - setDisableButtonClick(['IN_CALL', 'ON_HOLD'].includes(callerInfo.state)); - }, [callerInfo]); - - useEffect(() => { - let agentEnabled = false; - const state = voipClient.getRegistrarState(); - if (state === 'registered') { - agentEnabled = true; - } - setAgentEnabled(agentEnabled); - setRegistered(agentEnabled); - }, [voipClient]); - - // TODO: move registration flow to context provider - const handleVoipCallStatusChange = useMutableCallback((): void => { - if (disableButtonClick) { - return; - } - // TODO: backend set voip call status - // voipClient.setVoipCallStatus(!registered); - if (agentEnabled) { - callerInfo.state === 'OFFER_RECEIVED' && callActions.reject(); - setAgentEnabled(false); - voipClient.unregister(); - return; - } - setAgentEnabled(true); - voipClient.register(); - }); - - const onUnregistrationError = useMutableCallback((): void => { - setRegistered(false); - setAgentEnabled(false); - }); - - const onUnregistered = useMutableCallback((): void => { - setRegistered(!registered); - }); - - const onRegistrationError = useMutableCallback((): void => { - setRegistered(false); - setAgentEnabled(false); - }); - - const onRegistered = useMutableCallback((): void => { - setRegistered(!registered); - }); - - const onNetworkConnected = useMutableCallback((): void => { - setDisableButtonClick(['IN_CALL', 'ON_HOLD'].includes(callerInfo.state)); - setNetworkStatus('online'); - }); - - const onNetworkDisconnected = useMutableCallback((): void => { - setDisableButtonClick(true); - setNetworkStatus('offline'); - }); - - useEffect(() => { - if (!voipClient) { - return; - } - voipClient.on('registered', onRegistered); - voipClient.on('registrationerror', onRegistrationError); - voipClient.on('unregistered', onUnregistered); - voipClient.on('unregistrationerror', onUnregistrationError); - voipClient.onNetworkEvent('connected', onNetworkConnected); - voipClient.onNetworkEvent('disconnected', onNetworkDisconnected); - voipClient.onNetworkEvent('connectionerror', onNetworkDisconnected); - voipClient.onNetworkEvent('localnetworkonline', onNetworkConnected); - voipClient.onNetworkEvent('localnetworkoffline', onNetworkDisconnected); - - return (): void => { - voipClient.off('registered', onRegistered); - voipClient.off('registrationerror', onRegistrationError); - voipClient.off('unregistered', onUnregistered); - voipClient.off('unregistrationerror', onUnregistrationError); - voipClient.offNetworkEvent('connected', onNetworkConnected); - voipClient.offNetworkEvent('disconnected', onNetworkDisconnected); - voipClient.offNetworkEvent('connectionerror', onNetworkDisconnected); - voipClient.offNetworkEvent('localnetworkonline', onNetworkConnected); - voipClient.offNetworkEvent('localnetworkoffline', onNetworkDisconnected); - }; - }, [onRegistered, onRegistrationError, onUnregistered, onUnregistrationError, voipClient, onNetworkConnected, onNetworkDisconnected]); + color: getColor(), + }; - return ; + return ; }; diff --git a/apps/meteor/client/sidebar/sections/hooks/useVoipAgent.ts b/apps/meteor/client/sidebar/sections/hooks/useVoipAgent.ts new file mode 100644 index 000000000000..c61b65bc237e --- /dev/null +++ b/apps/meteor/client/sidebar/sections/hooks/useVoipAgent.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { VoIPAgentContext, VoIPAgentContextValue } from '../../../contexts/VoIPAgentContext'; + +export const useVoipAgent = (): VoIPAgentContextValue => useContext(VoIPAgentContext); diff --git a/yarn.lock b/yarn.lock index ea334bb70a38..e18b3ed257e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7805,7 +7805,7 @@ __metadata: human-interval: ~1.0.0 moment-timezone: ~0.5.27 mongodb: ~3.5.0 - checksum: acb4ebb7e7356f6e53e810d821eb6aa3d88bbfb9e85183e707517bee6d1eea1f189f38bdf0dd2b91360492ab7643134d510c320d2523d86596498ab98e59735b + checksum: f5f68008298f9482631f1f494e392cd6b8ba7971a3b0ece81ae2abe60f53d67973ff4476156fa5c9c41b8b58c4ccd284e95c545e0523996dfd05f9a80b843e07 languageName: node linkType: hard