From 91092a2c31410c3d924e4067cd101a83aab4f06e Mon Sep 17 00:00:00 2001 From: Sua Yoo Date: Mon, 5 Oct 2020 12:04:34 -0700 Subject: [PATCH] Mute/unmute conference participant in UI (#36) * add mute method * add unmute method * add to FE calls service * add response interface * remove extraneous conference method * remove extraneous conference method * add muted column * add mute/unmute button * set initial muted value * fix agent sip username showing in ui * save whether muted or unmuted Co-authored-by: Deivid Veloso --- .../server/controllers/calls.controller.ts | 10 ++ call-center/server/entities/callLeg.entity.ts | 3 + .../web-client/src/components/ActiveCall.tsx | 155 +++++++++++------- .../web-client/src/interfaces/ICallLeg.ts | 1 + 4 files changed, 112 insertions(+), 57 deletions(-) diff --git a/call-center/server/controllers/calls.controller.ts b/call-center/server/controllers/calls.controller.ts index 6685487..7ab4a63 100644 --- a/call-center/server/controllers/calls.controller.ts +++ b/call-center/server/controllers/calls.controller.ts @@ -159,6 +159,10 @@ class CallsController { call_control_ids: [appCall.telnyxCallControlId], }); + // Mark call as muted in app db + appCall.muted = true; + await callLegRepository.save(appCall); + res.json({ data: appCall, }); @@ -197,6 +201,10 @@ class CallsController { call_control_ids: [appCall.telnyxCallControlId], }); + // Mark call as unmuted in app db + appCall.muted = false; + await callLegRepository.save(appCall); + res.json({ data: appCall, }); @@ -292,6 +300,7 @@ class CallsController { appIncomingCallLeg.direction = CallLegDirection.INCOMING; appIncomingCallLeg.telnyxCallControlId = call_control_id; appIncomingCallLeg.telnyxConnectionId = connection_id; + appIncomingCallLeg.muted = false; await callLegRepository.save(appIncomingCallLeg); @@ -467,6 +476,7 @@ class CallsController { appAgentCallLeg.status = CallLegStatus.ACTIVE; appAgentCallLeg.telnyxCallControlId = telnyxOutgoingCall.call_control_id; appAgentCallLeg.telnyxConnectionId = connectionId; + appAgentCallLeg.muted = false; appAgentCallLeg.conference = await conferenceRepository.findOneOrFail( appConferenceId ); diff --git a/call-center/server/entities/callLeg.entity.ts b/call-center/server/entities/callLeg.entity.ts index 00a8ca1..36d444d 100644 --- a/call-center/server/entities/callLeg.entity.ts +++ b/call-center/server/entities/callLeg.entity.ts @@ -42,6 +42,9 @@ export class CallLeg { @Column() telnyxConnectionId!: string; + @Column() + muted!: boolean; + @ManyToOne((type) => Conference, (conference) => conference.callLegs, { cascade: ['update'], }) diff --git a/call-center/web-client/src/components/ActiveCall.tsx b/call-center/web-client/src/components/ActiveCall.tsx index 68c5bf7..ccb3219 100644 --- a/call-center/web-client/src/components/ActiveCall.tsx +++ b/call-center/web-client/src/components/ActiveCall.tsx @@ -6,7 +6,11 @@ import Agents from './Agents'; import './ActiveCall.css'; import useAgents from '../hooks/useAgents'; import useInterval from '../hooks/useInterval'; -import { hangup as appHangup } from '../services/callsService'; +import { + hangup as appHangup, + mute as appMute, + unmute as appUnmute, +} from '../services/callsService'; import { getConference } from '../services/conferencesService'; import IConference from '../interfaces/IConference'; import { CallLegDirection, CallLegStatus } from '../interfaces/ICallLeg'; @@ -35,9 +39,16 @@ interface IActiveCallConference { interface IConferenceParticipant { displayName?: string; + muted?: boolean; participant: string; } +interface IMuteUnmuteButton { + isMuted?: boolean; + mute: () => void; + unmute: () => void; +} + function useActiveConference(sipUsername: string) { let [loading, setLoading] = useState(true); let [error, setError] = useState(); @@ -62,6 +73,26 @@ function useActiveConference(sipUsername: string) { return { loading, error, conference }; } +function MuteUnmuteButton({ isMuted, mute, unmute }: IMuteUnmuteButton) { + return ( + + ); +} + function ActiveCallConference({ sipUsername, callDestination, @@ -74,12 +105,12 @@ function ActiveCallConference({ error: conferenceError, conference, } = useActiveConference(sipUsername); - let [participant, setParticipant] = useState(''); + let [newParticipant, setNewParticipant] = useState(''); const handleChangeDestination = ( event: React.ChangeEvent ) => { - setParticipant(event.target.value); + setNewParticipant(event.target.value); }; const addToCall = (destination: string) => @@ -94,51 +125,63 @@ function ActiveCallConference({ to: destination, }); - const removeFromCall = (participant: string) => { + const removeParticipant = (participant: string) => { appHangup({ participant }); }; + const muteParticipant = (participant: string) => { + appMute({ participant }); + }; + const unmuteParticipant = (participant: string) => { + appUnmute({ participant }); + }; + const confirmRemove = (participant: string) => { let result = window.confirm( `Are you sure you want to remove ${participant} from this call?` ); if (result) { - removeFromCall(participant); + removeParticipant(participant); } }; const handleAddDestination = (e: any) => { e.preventDefault(); - addToCall(participant); + addToCall(newParticipant); }; let conferenceParticipants: IConferenceParticipant[] = useMemo(() => { if (conference) { let otherParticipants = conference.callLegs .filter((callLeg) => callLeg.status === CallLegStatus.ACTIVE) - .map((callLeg) => - callLeg.direction === CallLegDirection.INCOMING - ? callLeg.from - : callLeg.to - ) + .map((callLeg) => ({ + muted: callLeg.muted, + participant: + callLeg.direction === CallLegDirection.INCOMING + ? callLeg.from + : callLeg.to, + })) .filter( - (participant) => participant !== `sip:${sipUsername}@sip.telnyx.com` + ({ participant }) => + participant !== `sip:${sipUsername}@sip.telnyx.com` ) - .map((participant) => { + .map(({ muted, participant }) => { let agent = agents?.find((agent) => participant.includes(agent.sipUsername) ); if (agent) { return { + muted, displayName: agent.name || agent.sipUsername, participant: `sip:${agent.sipUsername}@sip.telnyx.com`, }; } return { + muted, participant, }; }); @@ -161,42 +204,49 @@ function ActiveCallConference({ useEffect(() => { if ( - participant && + newParticipant && conferenceParticipants .map(({ participant }) => participant) - .includes(participant) + .includes(newParticipant) ) { - setParticipant(''); + setNewParticipant(''); } }, [conferenceParticipants]); return (
- {conferenceParticipants.map(({ displayName, participant }, index) => ( -
-
- {index !== 0 ? ( - & - ) : null} - - {displayName || participant} - -
- {index !== 0 && ( -
- + {conferenceParticipants.map( + ({ muted, displayName, participant }, index) => ( +
+
+ {index !== 0 ? ( + & + ) : null} + + {displayName || participant} +
- )} - {participant} -
- ))} + {index !== 0 && ( +
+ muteParticipant(participant)} + unmute={() => unmuteParticipant(participant)} + /> + + +
+ )} +
+ ) + )}
@@ -213,7 +263,7 @@ function ActiveCallConference({ className="App-input" name="destination" type="text" - value={participant} + value={newParticipant} placeholder="Phone number or SIP URI" required onChange={handleChangeDestination} @@ -249,12 +299,12 @@ function ActiveCall({ const handleRejectClick = () => hangup(); const handleHangupClick = () => hangup(); - const handleMuteClick = () => { + const muteSelf = () => { setIsMuted(true); muteAudio(); }; - const handleUnmuteClick = () => { + const unmuteSelf = () => { unmuteAudio(); setIsMuted(false); }; @@ -326,21 +376,12 @@ function ActiveCall({ > Hangup - + +
)} diff --git a/call-center/web-client/src/interfaces/ICallLeg.ts b/call-center/web-client/src/interfaces/ICallLeg.ts index 3a7df3b..968b49e 100644 --- a/call-center/web-client/src/interfaces/ICallLeg.ts +++ b/call-center/web-client/src/interfaces/ICallLeg.ts @@ -18,6 +18,7 @@ export interface ICallLeg { direction: string; telnyxCallControlId: string; telnyxConnectionId: string; + muted: boolean; conference: IConference; }