Skip to content

Commit

Permalink
Multi-call refactor (#628)
Browse files Browse the repository at this point in the history
  • Loading branch information
dremin authored Sep 20, 2024
1 parent dea22c2 commit 1df4338
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 194 deletions.
17 changes: 9 additions & 8 deletions docs/docs/feature-library/multi-call.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ sidebar_label: multi-call
title: multi-call
---

Out of the box, Flex does not allow a single worker to have more than one call active at once. Due to this limitation, another worker cannot transfer a call to another worker if they already are on a call. This feature allows a worker to handle two calls at once, and will automatically place other calls on hold when accepting a new call. As a result, a worker can gracefully handle a transferred call while already assigned another call.
:::info Flex UI 2.8 or later required
This feature requires Flex UI 2.8 or later, as it depends on Twilio Voice SDK features that were unavailable prior to that version.
:::

Out of the box, Flex does not allow a single worker to have more than one call active at once. Due to this limitation, another worker cannot transfer a call to another worker if they already are on a call. This feature allows a worker to handle more than one call at once, and will automatically place other calls on hold when accepting a new call. As a result, a worker can gracefully handle a transferred call while already assigned another call, for example.

![Multi-call demo](/img/features/multi-call/multi-call.gif)

Expand All @@ -13,23 +17,20 @@ This feature requires some TaskRouter configuration changes in addition to Flex

### TaskRouter

First, agents will need their capacity for the `voice` channel to be increased from 1 to 2. This can be done via the console, API, Single Sign On configuration, or via the `supervisor-capacity` plugin feature. This will enable TaskRouter to successfully transfer a call to a worker that already has another call.
First, agents will need their capacity for the `voice` channel to be increased from 1 to a larger number. This can be done via the console, API, Single Sign On configuration, or via the `supervisor-capacity` plugin feature. This will enable TaskRouter to successfully transfer or assign a call to a worker that already has another call.

Now that workers can accept multiple calls, we need to update the TaskRouter workflow(s) so that agents are not routed multiple calls from the queue. For each workflow filter, set the target worker expression to `worker.channel.voice.available_capacity_percentage == 100`. If you already have a target worker expression defined, you will need to combine the logic with `AND`.
Now that workers can accept multiple calls, if you want to route inbound calls to only workers not already on a call, we need to update the TaskRouter workflow(s) so that agents are not routed multiple calls from the queue. For each workflow filter, set the target worker expression to `worker.channel.voice.available_capacity_percentage == 100`. If you already have a target worker expression defined, you will need to combine the logic with `AND`.

> **Warning**
> Transfers to queues will not use the above configured worker expression. If workers in transfer queues do not all have their capacity set to 1, customize the queue transfer directory to instead transfer to workflows. Otherwise, transfers to queues may be assigned to workers already on calls.
### Flex configuration

In your flex-config file(s), two changes need to be made:

1. Enable the `multi_call` feature
2. Disable the `allowIncomingWhileBusy` voice SDK option (yes, this is counter-intuitive!)
In your flex-config file(s), all you need to do is enable the `multi_call` feature.

## How it works

The reason that Flex does not support multiple simultaneous calls out-of-the-box is due to a limitation in the Twilio Voice JavaScript SDK used by Flex. To work around this limitation, the `multi-call` feature instantiates a second Voice SDK `Device` to handle a second incoming call. This works because disabling `allowIncomingWhileBusy` prevents the Voice SDK instance managed by Flex from receiving a second inbound call, allowing our second instance to handle it gracefully.
The reason that Flex does not support multiple simultaneous calls out-of-the-box is due to a limitation in the Twilio Voice JavaScript SDK used by Flex. To work around this limitation, when another call is accepted, the `multi-call` feature instantiates a second Voice SDK `Device` and forwards the call to it. This new instance of the Voice SDK is then passed the same configuration options as the built-in Voice SDK instance.

Due to the Voice SDK limitation, Flex's state maintains an assumption that only one call at a time may be considered active. To work around this, the `multi-call` feature changes the active call in Flex state whenever the selected task changes.

Expand Down
5 changes: 0 additions & 5 deletions flex-config/ui_attributes.common.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
{
"sdkOptions": {
"voice": {
"allowIncomingWhileBusy": true
}
},
"custom_data": {
"serverless_functions_domain": "<YOUR_SERVERLESS_DOMAIN>",
"common": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ export const actionHook = function handleConferenceHangup(flex: typeof Flex, _ma
return updatedTask.conference;
};

// check if worker hanging up is last worker on the call
if (conference && conference.liveWorkerCount === 1) {
// check if worker hanging up is last worker on the call and this is a multi-party call
if (
conference &&
conference.liveWorkerCount === 1 &&
conference.liveParticipantCount - conference.liveWorkerCount > 1
) {
// if so, ensure no other participants are on hold as
// no external parties will be able to remove them from being on hold.
conference.participants.forEach(async (participant: Flex.ConferenceParticipant) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { VolumeOnIcon } from '@twilio-paste/icons/esm/VolumeOnIcon';
import { AgentIcon } from '@twilio-paste/icons/esm/AgentIcon';

import { isInputSelectEnabled } from '../../config';
import { SecondDevice } from '../../../multi-call/helpers/MultiCallHelper';
import { MultiCallDevices } from '../../../multi-call/helpers/MultiCallHelper';
import { isFeatureEnabled as isMultiCallEnabled } from '../../../multi-call/config';
import { StringTemplates } from '../../flex-hooks/strings';

Expand Down Expand Up @@ -47,9 +47,11 @@ const DeviceManager: React.FunctionComponent = () => {

setSelectedDevice(selectedDevice, voiceClient);

// set SecondDevice options if multi-call feature is enabled
if (isMultiCallEnabled() && SecondDevice) {
setSelectedDevice(selectedDevice, SecondDevice);
// set device options for multi-call feature if enabled
if (isMultiCallEnabled()) {
MultiCallDevices.forEach((device) => {
setSelectedDevice(selectedDevice, device);
});
}

menu.hide();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Flex from '@twilio/flex-ui';

import { holdOtherCalls } from '../../helpers/MultiCallHelper';
import { FlexActionEvent, FlexAction } from '../../../../types/feature-loader';

export const actionEvent = FlexActionEvent.before;
export const actionName = FlexAction.AcceptTask;
export const actionHook = function holdOtherCallsOnAcceptTask(flex: typeof Flex) {
flex.Actions.addListener(`${actionEvent}${actionName}`, async () => {
holdOtherCalls();
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Flex from '@twilio/flex-ui';

import { holdOtherCalls } from '../../helpers/MultiCallHelper';
import { FlexActionEvent, FlexAction } from '../../../../types/feature-loader';

export const actionEvent = FlexActionEvent.before;
export const actionName = FlexAction.MonitorCall;
export const actionHook = function holdOtherCallsOnMonitorCall(flex: typeof Flex) {
flex.Actions.addListener(`${actionEvent}${actionName}`, async () => {
holdOtherCalls();
});
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Flex from '@twilio/flex-ui';

import { FlexDeviceCall, getMyCallSid, SecondDeviceCall } from '../../helpers/MultiCallHelper';
import { getMyCallSid, getCall } from '../../helpers/MultiCallHelper';
import { FlexActionEvent, FlexAction } from '../../../../types/feature-loader';

export const actionEvent = FlexActionEvent.before;
Expand All @@ -25,10 +25,9 @@ export const actionHook = function handleMultiCallSelectTask(flex: typeof Flex,
}

// update state with the currently selected call
if (SecondDeviceCall && callSid === SecondDeviceCall.parameters.CallSid) {
manager.store.dispatch({ type: 'PHONE_ADD_CALL', payload: SecondDeviceCall });
} else if (FlexDeviceCall && callSid === FlexDeviceCall.parameters.CallSid) {
manager.store.dispatch({ type: 'PHONE_ADD_CALL', payload: FlexDeviceCall });
const call = getCall(callSid);
if (call) {
manager.store.dispatch({ type: 'PHONE_ADD_CALL', payload: call });
}
});
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import * as Flex from '@twilio/flex-ui';
import { SSOTokenPayload } from '@twilio/flex-ui/src/core/TokenStorage';

import { SecondDevice } from '../../helpers/MultiCallHelper';
import { MultiCallDevices } from '../../helpers/MultiCallHelper';
import { FlexEvent } from '../../../../types/feature-loader';
import logger from '../../../../utils/logger';

export const eventName = FlexEvent.tokenUpdated;
export const eventHook = (flex: typeof Flex, manager: Flex.Manager, tokenPayload: SSOTokenPayload) => {
if (!SecondDevice) return;

if (SecondDevice?.state === 'destroyed') {
return;
}

SecondDevice?.updateToken(tokenPayload.token);
MultiCallDevices.forEach((device) => {
device.updateToken(tokenPayload.token);
});

logger.info('[multi-call] Token updated');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Flex from '@twilio/flex-ui';

export const eventName = Flex.NotificationEvent.beforeAddNotification;
export const notificationEventHook = (flex: typeof Flex, manager: Flex.Manager, notification: any, cancel: any) => {
// Normally Flex only supports 1 call, so it shows an error notification upon receiving a second call.
// We want to suppress this notification when multi-call is enabled
if (notification.id === 'SecondVoiceCallIncoming') {
cancel();
}
};

This file was deleted.

This file was deleted.

Loading

0 comments on commit 1df4338

Please sign in to comment.