diff --git a/client/contexts/CallContext.ts b/client/contexts/CallContext.ts index 4be9c138a0462..be9409b95041e 100644 --- a/client/contexts/CallContext.ts +++ b/client/contexts/CallContext.ts @@ -22,10 +22,12 @@ type CallContextReady = { ready: true; voipClient: VoIPUser; actions: CallActionsType; + queueName: string; queueCounter: number; openedRoomInfo: { v: { token?: string }; rid: string }; openWrapUpModal: () => void; - openRoom: (caller: ICallerInfo) => IVoipRoom['_id']; + openRoom: (rid: IVoipRoom['_id']) => void; + createRoom: (caller: ICallerInfo) => IVoipRoom['_id']; closeRoom: (data: { comment?: string; tags?: string[] }) => void; }; type CallContextError = { @@ -104,6 +106,16 @@ export const useCallerInfo = (): VoIpCallerInfo => { return useSubscription(subscription); }; +export const useCallCreateRoom = (): CallContextReady['createRoom'] => { + const context = useContext(CallContext); + + if (!isCallContextReady(context)) { + throw new Error('useCallerInfo only if Calls are enabled and ready'); + } + + return context.createRoom; +}; + export const useCallOpenRoom = (): CallContextReady['openRoom'] => { const context = useContext(CallContext); @@ -133,6 +145,16 @@ export const useCallClient = (): VoIPUser => { return context.voipClient; }; +export const useQueueName = (): CallContextReady['queueName'] => { + const context = useContext(CallContext); + + if (!isCallContextReady(context)) { + throw new Error('useQueueInfo only if Calls are enabled and ready'); + } + + return context.queueName; +}; + export const useQueueCounter = (): CallContextReady['queueCounter'] => { const context = useContext(CallContext); diff --git a/client/providers/CallProvider/CallProvider.tsx b/client/providers/CallProvider/CallProvider.tsx index 14ec7d084bc68..eb3e7a6fbc36b 100644 --- a/client/providers/CallProvider/CallProvider.tsx +++ b/client/providers/CallProvider/CallProvider.tsx @@ -7,6 +7,7 @@ import { CustomSounds } from '../../../app/custom-sounds/client'; import { getUserPreference } from '../../../app/utils/client'; import { IVoipRoom } from '../../../definition/IRoom'; import { IUser } from '../../../definition/IUser'; +import { ICallerInfo } from '../../../definition/voip/ICallerInfo'; import { WrapUpCallModal } from '../../components/voip/modal/WrapUpCallModal'; import { CallContext, CallContextValue } from '../../contexts/CallContext'; import { useSetModal } from '../../contexts/ModalContext'; @@ -43,7 +44,8 @@ export const CallProvider: FC = ({ children }) => { const AudioTagPortal: FC = ({ children }) => useMemo(() => createPortal(children, document.body), [children]); - const [queueCounter, setQueueCounter] = useState(''); + const [queueName, setQueueName] = useState(''); + const [queueCounter, setQueueCounter] = useState(0); const setModal = useSetModal(); @@ -71,7 +73,8 @@ export const CallProvider: FC = ({ children }) => { queuedcalls: string; }): Promise => { queueAggregator.queueJoined(joiningDetails); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueName(joiningDetails.queuename); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/callerjoined`, handleQueueJoined); @@ -93,7 +96,7 @@ export const CallProvider: FC = ({ children }) => { const handleAgentConnected = (queue: { queuename: string; queuedcalls: string; waittimeinqueue: string }): void => { queueAggregator.callPickedup(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/agentconnected`, handleAgentConnected); @@ -115,7 +118,8 @@ export const CallProvider: FC = ({ children }) => { const handleMemberAdded = (queue: { queuename: string; queuedcalls: string }): void => { queueAggregator.memberAdded(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueName(queue.queuename); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/queuememberadded`, handleMemberAdded); @@ -137,7 +141,7 @@ export const CallProvider: FC = ({ children }) => { const handleMemberRemoved = (queue: { queuename: string; queuedcalls: string }): void => { queueAggregator.memberRemoved(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/queuememberremoved`, handleMemberRemoved); @@ -159,7 +163,7 @@ export const CallProvider: FC = ({ children }) => { const handleCallAbandon = (queue: { queuename: string; queuedcallafterabandon: string }): void => { queueAggregator.queueAbandoned(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/callabandoned`, handleCallAbandon); @@ -233,6 +237,10 @@ export const CallProvider: FC = ({ children }) => { const [roomInfo, setRoomInfo] = useState<{ v: { token?: string }; rid: string }>(); + const openRoom = (rid: IVoipRoom['_id']): void => { + roomCoordinator.openRouteLink('v', { rid }); + }; + const contextValue: CallContextValue = useMemo(() => { if (!voipEnabled) { return { @@ -269,6 +277,7 @@ export const CallProvider: FC = ({ children }) => { registrationInfo, voipClient, queueCounter, + queueName, actions: { mute: (): Promise => voipClient.muteCall(true), // voipClient.mute(), unmute: (): Promise => voipClient.muteCall(false), // voipClient.unmute() @@ -279,7 +288,8 @@ export const CallProvider: FC = ({ children }) => { remoteAudioMediaRef.current && voipClient.acceptCall({ remoteMediaElement: remoteAudioMediaRef.current }), reject: (): Promise => voipClient.rejectCall(), }, - openRoom: async (caller): Promise => { + openRoom, + createRoom: async (caller: ICallerInfo): Promise => { if (user) { const { visitor } = await visitorEndpoint({ visitor: { @@ -289,7 +299,7 @@ export const CallProvider: FC = ({ children }) => { }, }); const voipRoom = visitor && (await voipEndpoint({ token: visitor.token, agentId: user._id })); - voipRoom.room && roomCoordinator.openRouteLink(voipRoom.room.t, { rid: voipRoom.room._id, name: voipRoom.room.name }); + openRoom(voipRoom.room._id); voipRoom.room && setRoomInfo({ v: { token: voipRoom.room.v.token }, rid: voipRoom.room._id }); const queueAggregator = result.voipClient.getAggregator(); if (queueAggregator) { @@ -299,7 +309,7 @@ export const CallProvider: FC = ({ children }) => { } return ''; }, - closeRoom: async ({ comment, tags }): Promise => { + closeRoom: async ({ comment, tags }: { comment: string; tags: string[] }): Promise => { roomInfo && (await voipCloseRoomEndpoint({ rid: roomInfo.rid, token: roomInfo.v.token || '', comment: comment || '', tags })); homeRoute.push({}); const queueAggregator = result.voipClient.getAggregator(); @@ -309,7 +319,19 @@ export const CallProvider: FC = ({ children }) => { }, openWrapUpModal, }; - }, [queueCounter, voipEnabled, homeRoute, openWrapUpModal, result, roomInfo, user, visitorEndpoint, voipCloseRoomEndpoint, voipEndpoint]); + }, [ + voipEnabled, + result, + roomInfo, + queueCounter, + queueName, + openWrapUpModal, + user, + visitorEndpoint, + voipEndpoint, + voipCloseRoomEndpoint, + homeRoute, + ]); return ( diff --git a/client/sidebar/footer/voip/VoipFooter.stories.tsx b/client/sidebar/footer/voip/VoipFooter.stories.tsx index d13b9477ee17c..c9bb15dd8886e 100644 --- a/client/sidebar/footer/voip/VoipFooter.stories.tsx +++ b/client/sidebar/footer/voip/VoipFooter.stories.tsx @@ -25,10 +25,9 @@ const tooltips = { endCall: 'End Call', }; -export const Default = (): ReactElement => { +export const IncomingCall = (): ReactElement => { const [muted, toggleMic] = useState(false); const [paused, togglePause] = useState(false); - const [callsInQueue] = useState('2'); return ( @@ -47,11 +46,52 @@ export const Default = (): ReactElement => { toggleMic={toggleMic} togglePause={togglePause} tooltips={tooltips} + createRoom={() => ''} openRoom={() => ''} - callsInQueue={callsInQueue} + callsInQueue='2 Calls In Queue' openWrapUpCallModal={() => null} dispatchEvent={() => null} openedRoomInfo={{ v: { token: '' }, rid: '' }} + anonymousText={'Anonymous'} + /> + + ); +}; + +export const InCall = (): ReactElement => { + const [muted, toggleMic] = useState(false); + const [paused, togglePause] = useState(false); + const getSubtitle = () => { + if (paused) { + return 'On Hold'; + } + return 'In Progress'; + }; + + return ( + + ''} + openRoom={() => ''} + callsInQueue='2 Calls In Queue' + openWrapUpCallModal={() => null} + dispatchEvent={() => null} + openedRoomInfo={{ v: { token: '' }, rid: '' }} + anonymousText={'Anonymous'} /> ); diff --git a/client/sidebar/footer/voip/VoipFooter.tsx b/client/sidebar/footer/voip/VoipFooter.tsx index c9fb32d721989..1079216259727 100644 --- a/client/sidebar/footer/voip/VoipFooter.tsx +++ b/client/sidebar/footer/voip/VoipFooter.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box, Button, ButtonGroup, Icon, SidebarFooter } from '@rocket.chat/fuselage'; import React, { ReactElement } from 'react'; @@ -26,9 +27,11 @@ type VoipFooterPropsType = { callsInQueue: string; openWrapUpCallModal: () => void; - openRoom: (caller: ICallerInfo) => IVoipRoom['_id']; + createRoom: (caller: ICallerInfo) => IVoipRoom['_id']; + openRoom: (rid: IVoipRoom['_id']) => void; dispatchEvent: (params: { event: VoipClientEvents; rid: string; comment?: string }) => void; openedRoomInfo: { v: { token?: string | undefined }; rid: string }; + anonymousText: string; }; export const VoipFooter = ({ @@ -42,101 +45,133 @@ export const VoipFooter = ({ toggleMic, togglePause, tooltips, + createRoom, openRoom, callsInQueue, openWrapUpCallModal, dispatchEvent, openedRoomInfo, -}: VoipFooterPropsType): ReactElement => ( - - - {callsInQueue} - - - - {title} - - {(callerState === 'IN_CALL' || callerState === 'ON_HOLD') && ( - - - - - )} - - - - - {caller.callerName} + anonymousText, +}: VoipFooterPropsType): ReactElement => { + const cssClickable = + callerState === 'IN_CALL' || callerState === 'ON_HOLD' + ? css` + cursor: pointer; + ` + : ''; + + return ( + + { + if (callerState === 'IN_CALL' || callerState === 'ON_HOLD') { + openRoom(openedRoomInfo.rid); + } + }} + > + + {callsInQueue} - - {subtitle} + + + {title} + + {(callerState === 'IN_CALL' || callerState === 'ON_HOLD') && ( + + + + + )} - + + + + {caller.callerName || caller.callerId || anonymousText} + + + {subtitle} + + - - {callerState === 'IN_CALL' && ( - - )} - {callerState === 'OFFER_RECEIVED' && ( - - )} - {callerState === 'OFFER_RECEIVED' && ( - - )} - - - -); + return callActions.end(); + }} + > + + + )} + {callerState === 'OFFER_RECEIVED' && ( + + )} + {callerState === 'OFFER_RECEIVED' && ( + + )} + + + + + ); +}; diff --git a/client/sidebar/footer/voip/index.tsx b/client/sidebar/footer/voip/index.tsx index c9523916fa3f7..bc6a6fb3e20f8 100644 --- a/client/sidebar/footer/voip/index.tsx +++ b/client/sidebar/footer/voip/index.tsx @@ -1,11 +1,13 @@ -import React, { ReactElement, useCallback, useState } from 'react'; +import React, { ReactElement, useCallback, useMemo, useState } from 'react'; import { useCallActions, + useCallCreateRoom, useCallerInfo, useCallOpenRoom, useOpenedRoomInfo, useQueueCounter, + useQueueName, useWrapUpModal, } from '../../../contexts/CallContext'; import { useEndpoint } from '../../../contexts/ServerContext'; @@ -18,8 +20,10 @@ export const VoipFooter = (): ReactElement | null => { const callActions = useCallActions(); const dispatchEvent = useEndpoint('POST', 'voip/events'); + const createRoom = useCallCreateRoom(); const openRoom = useCallOpenRoom(); const queueCounter = useQueueCounter(); + const queueName = useQueueName(); const openWrapUpCallModal = useWrapUpModal(); const openedRoomInfo = useOpenedRoomInfo(); @@ -63,6 +67,18 @@ export const VoipFooter = (): ReactElement | null => { endCall: t('End_Call'), }; + const getCallsInQueueText = useMemo((): string => { + if (queueCounter === 0) { + return t('Calls_in_queue_empty'); + } + + if (queueCounter === 1) { + return t('Calls_in_queue', { calls: queueCounter }); + } + + return t('Calls_in_queue_plural', { calls: queueCounter }); + }, [queueCounter, t]); + if (!('caller' in callerInfo)) { return null; } @@ -72,18 +88,20 @@ export const VoipFooter = (): ReactElement | null => { caller={callerInfo.caller} callerState={callerInfo.state} callActions={callActions} - title={t('Phone_call')} + title={queueName || t('Phone_call')} subtitle={getSubtitle()} muted={muted} paused={paused} toggleMic={toggleMic} togglePause={togglePause} tooltips={tooltips} + createRoom={createRoom} openRoom={openRoom} - callsInQueue={t('Calls_in_queue', { calls: queueCounter })} + callsInQueue={getCallsInQueueText} openWrapUpCallModal={openWrapUpCallModal} dispatchEvent={dispatchEvent} openedRoomInfo={openedRoomInfo} + anonymousText={t('Anonymous')} /> ); }; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 880f61a8e3c00..ef24ba465645e 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -350,6 +350,7 @@ "And_more": "And __length__ more", "Animals_and_Nature": "Animals & Nature", "Announcement": "Announcement", + "Anonymous": "Anonymous", "Answer_call": "Answer Call", "API": "API", "API_Add_Personal_Access_Token": "Add new Personal Access Token", @@ -712,7 +713,9 @@ "Call": "Call", "Calling": "Calling", "Call_Center": "Call Center", - "Calls_in_queue": "__calls__ Calls In Queue", + "Calls_in_queue": "__calls__ Call In Queue", + "Calls_in_queue_plural": "__calls__ Calls In Queue", + "Calls_in_queue_empty": "Queue Is Empty", "Call_declined": "Call Declined!", "Call_Information": "Call Information", "Call_provider": "Call Provider", @@ -4949,4 +4952,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Some of the services will be unavailable or will require manual setup", "onboarding.form.standaloneServerForm.publishOwnApp": "In order to send push notitications you need to compile and publish your own app to Google Play and App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Need to manually integrate with external services" -} \ No newline at end of file +}