Skip to content

Commit

Permalink
Mute/unmute conference participant in UI (#36)
Browse files Browse the repository at this point in the history
* 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 <veloso.deivid@gmail.com>
  • Loading branch information
SuaYoo and DeividVeloso committed Oct 5, 2020
1 parent 89c5601 commit 91092a2
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 57 deletions.
10 changes: 10 additions & 0 deletions call-center/server/controllers/calls.controller.ts
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
);
Expand Down
3 changes: 3 additions & 0 deletions call-center/server/entities/callLeg.entity.ts
Expand Up @@ -42,6 +42,9 @@ export class CallLeg {
@Column()
telnyxConnectionId!: string;

@Column()
muted!: boolean;

@ManyToOne((type) => Conference, (conference) => conference.callLegs, {
cascade: ['update'],
})
Expand Down
155 changes: 98 additions & 57 deletions call-center/web-client/src/components/ActiveCall.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -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<boolean>(true);
let [error, setError] = useState<string | undefined>();
Expand All @@ -62,6 +73,26 @@ function useActiveConference(sipUsername: string) {
return { loading, error, conference };
}

function MuteUnmuteButton({ isMuted, mute, unmute }: IMuteUnmuteButton) {
return (
<button
type="button"
className="App-button App-button--small App-button--tertiary"
onClick={isMuted ? unmute : mute}
>
{isMuted ? (
<span role="img" aria-label={isMuted ? 'Unmute' : 'Mute'}>
🔇
</span>
) : (
<span role="img" aria-label={isMuted ? 'Unmute' : 'Mute'}>
🔈
</span>
)}
</button>
);
}

function ActiveCallConference({
sipUsername,
callDestination,
Expand All @@ -74,12 +105,12 @@ function ActiveCallConference({
error: conferenceError,
conference,
} = useActiveConference(sipUsername);
let [participant, setParticipant] = useState('');
let [newParticipant, setNewParticipant] = useState('');

const handleChangeDestination = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setParticipant(event.target.value);
setNewParticipant(event.target.value);
};

const addToCall = (destination: string) =>
Expand All @@ -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,
};
});
Expand All @@ -161,42 +204,49 @@ function ActiveCallConference({

useEffect(() => {
if (
participant &&
newParticipant &&
conferenceParticipants
.map(({ participant }) => participant)
.includes(participant)
.includes(newParticipant)
) {
setParticipant('');
setNewParticipant('');
}
}, [conferenceParticipants]);

return (
<div className="ActiveCall-conference">
<div>
{conferenceParticipants.map(({ displayName, participant }, index) => (
<div className="ActiveCall-participant-row">
<div className="ActiveCall-participant">
{index !== 0 ? (
<span className="ActiveCall-ampersand">&</span>
) : null}
<span className="ActiveCall-participant-name">
{displayName || participant}
</span>
</div>
{index !== 0 && (
<div>
<button
type="button"
className="App-button App-button--small App-button--danger"
onClick={() => confirmRemove(participant)}
>
Remove
</button>
{conferenceParticipants.map(
({ muted, displayName, participant }, index) => (
<div className="ActiveCall-participant-row">
<div className="ActiveCall-participant">
{index !== 0 ? (
<span className="ActiveCall-ampersand">&</span>
) : null}
<span className="ActiveCall-participant-name">
{displayName || participant}
</span>
</div>
)}
<span className="ActiveCall-participant-name">{participant}</span>
</div>
))}
{index !== 0 && (
<div>
<MuteUnmuteButton
isMuted={muted}
mute={() => muteParticipant(participant)}
unmute={() => unmuteParticipant(participant)}
/>

<button
type="button"
className="App-button App-button--small App-button--danger"
onClick={() => confirmRemove(participant)}
>
Remove
</button>
</div>
)}
</div>
)
)}
</div>

<div>
Expand All @@ -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}
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -326,21 +376,12 @@ function ActiveCall({
>
Hangup
</button>
<button
type="button"
className="App-button App-button--tertiary"
onClick={isMuted ? handleUnmuteClick : handleMuteClick}
>
{isMuted ? (
<span role="img" aria-label={isMuted ? 'Unmute' : 'Mute'}>
🔇
</span>
) : (
<span role="img" aria-label={isMuted ? 'Unmute' : 'Mute'}>
🔈
</span>
)}
</button>

<MuteUnmuteButton
isMuted={isMuted}
mute={muteSelf}
unmute={unmuteSelf}
/>
</div>
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions call-center/web-client/src/interfaces/ICallLeg.ts
Expand Up @@ -18,6 +18,7 @@ export interface ICallLeg {
direction: string;
telnyxCallControlId: string;
telnyxConnectionId: string;
muted: boolean;
conference: IConference;
}

Expand Down

0 comments on commit 91092a2

Please sign in to comment.