Skip to content

Commit ba70164

Browse files
authored
fix(call-control): implement-mute-unmute-actions (#492)
1 parent 09bb7d4 commit ba70164

15 files changed

Lines changed: 486 additions & 9 deletions

File tree

packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React from 'react';
1+
import React, {useState} from 'react';
22
import {ButtonCircle, TooltipNext, Text} from '@momentum-ui/react-collaboration';
33
import {Avatar, Icon} from '@momentum-design/components/dist/react';
4-
4+
import {MUTE_CALL, UNMUTE_CALL} from '../../constants';
55
import TaskTimer from '../../TaskTimer';
66
import {CallControlConsultComponentsProps} from '../../task.types';
77

@@ -14,7 +14,12 @@ const CallControlConsultComponent: React.FC<CallControlConsultComponentsProps> =
1414
isAgentBeingConsulted,
1515
isEndConsultEnabled,
1616
logger,
17+
muteUnmute,
18+
isMuted,
19+
onToggleConsultMute,
1720
}) => {
21+
const [isMuteDisabled, setIsMuteDisabled] = useState(false);
22+
1823
const timerKey = `timer-${startTimeStamp}`;
1924

2025
const handleTransfer = () => {
@@ -51,7 +56,35 @@ const CallControlConsultComponent: React.FC<CallControlConsultComponentsProps> =
5156
}
5257
};
5358

59+
const handleConsultMuteToggle = () => {
60+
setIsMuteDisabled(true);
61+
62+
try {
63+
onToggleConsultMute();
64+
} catch (error) {
65+
logger.error('Mute toggle failed:', {
66+
error,
67+
module: 'call-control-consult.tsx',
68+
method: 'handleConsultMuteToggle',
69+
});
70+
} finally {
71+
// Re-enable button after operation
72+
setTimeout(() => {
73+
setIsMuteDisabled(false);
74+
}, 500);
75+
}
76+
};
77+
5478
const buttons = [
79+
{
80+
key: 'mute',
81+
icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold',
82+
onClick: handleConsultMuteToggle,
83+
tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL,
84+
className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`,
85+
disabled: isMuteDisabled,
86+
shouldShow: muteUnmute,
87+
},
5588
{
5689
key: 'transfer',
5790
icon: 'next-bold',

packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,37 @@
137137
--mdc-icon-fill-color: var(--mds-color-theme-button-cancel-normal);
138138
}
139139

140+
.md-button-circle-wrapper.call-control-button-muted {
141+
width: 1rem;
142+
height: 1rem;
143+
min-width: 2rem;
144+
min-height: 2rem;
145+
padding: 0.5rem;
146+
border-radius: 50%;
147+
background-color: var(--mds-color-theme-button-secondary-normal);
148+
border: 1px solid var(--mds-color-theme-outline-cancel-normal);
149+
&:hover,
150+
&.hover {
151+
background-color: var(--mds-color-theme-button-secondary-hover);
152+
}
153+
.call-control-button-muted-icon {
154+
--mdc-icon-fill-color: var(--mds-color-theme-text-error-normal);
155+
}
156+
}
157+
158+
.md-button-circle-wrapper.call-control-button-muted[disabled],
159+
.md-button-circle-wrapper.call-control-button-muted.shallowDisabled,
160+
.md-button-circle-wrapper.call-control-button-muted:disabled {
161+
outline-color: var(--mds-color-theme-button-primary-disabled);
162+
border-color: var(--mds-color-theme-button-primary-disabled);
163+
opacity: 0.5;
164+
cursor: not-allowed;
165+
.call-control-button-muted-icon {
166+
--mdc-icon-fill-color: var(--mds--color-theme-button-secondary-normal);
167+
}
168+
}
169+
170+
140171
.wrapup-button {
141172
display: inline-flex;
142173
justify-content: center;

packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,23 @@ import {
2121
WRAP_UP_REASON,
2222
SELECT,
2323
SUBMIT_WRAP_UP,
24+
MUTE_CALL,
25+
UNMUTE_CALL,
2426
} from '../constants';
2527

2628
function CallControlComponent(props: CallControlComponentProps) {
2729
const [selectedWrapupReason, setSelectedWrapupReason] = useState<string | null>(null);
2830
const [selectedWrapupId, setSelectedWrapupId] = useState<string | null>(null);
2931
const [showAgentMenu, setShowAgentMenu] = useState(false);
3032
const [agentMenuType, setAgentMenuType] = useState<CallControlMenuType | null>(null);
33+
const [isMuteButtonDisabled, setIsMuteButtonDisabled] = useState(false);
3134

3235
const {
3336
currentTask,
3437
toggleHold,
3538
toggleRecording,
39+
toggleMute,
40+
isMuted,
3641
endCall,
3742
wrapupCall,
3843
wrapupCodes,
@@ -82,6 +87,25 @@ function CallControlComponent(props: CallControlComponentProps) {
8287
setIsHeld(!isHeld);
8388
};
8489

90+
const handleMuteToggle = () => {
91+
setIsMuteButtonDisabled(true);
92+
93+
try {
94+
toggleMute();
95+
} catch (error) {
96+
logger.error('Mute toggle failed:', {
97+
error,
98+
module: 'call-control.tsx',
99+
method: 'handleMuteToggle',
100+
});
101+
} finally {
102+
// Re-enable button after operation
103+
setTimeout(() => {
104+
setIsMuteButtonDisabled(false);
105+
}, 500);
106+
}
107+
};
108+
85109
const handleWrapupCall = () => {
86110
logger.info('CC-Widgets: CallControl: wrap-up submitted', {
87111
module: 'call-control.tsx',
@@ -151,6 +175,15 @@ function CallControlComponent(props: CallControlComponentProps) {
151175
const isTelephony = mediaType === 'telephony';
152176

153177
const buttons = [
178+
{
179+
id: 'mute',
180+
icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold',
181+
onClick: handleMuteToggle,
182+
tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL,
183+
className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`,
184+
disabled: isMuteButtonDisabled,
185+
isVisible: controlVisibility.muteUnmute,
186+
},
154187
{
155188
id: 'hold',
156189
icon: isHeld ? 'play-bold' : 'pause-bold',

packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const CallControlCADComponent: React.FC<CallControlComponentProps> = (props) =>
4141
lastTargetType,
4242
controlVisibility,
4343
logger,
44+
isMuted,
45+
toggleMute,
4446
} = props;
4547

4648
const formatTime = (time: number): string => {
@@ -207,6 +209,9 @@ const CallControlCADComponent: React.FC<CallControlComponentProps> = (props) =>
207209
isAgentBeingConsulted={!consultAccepted}
208210
isEndConsultEnabled={isEndConsultEnabled}
209211
logger={logger}
212+
muteUnmute={controlVisibility.muteUnmute}
213+
isMuted={isMuted}
214+
onToggleConsultMute={toggleMute}
210215
/>
211216
</div>
212217
)}

packages/contact-center/cc-components/src/components/task/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const WRAP_UP_INTERACTION = 'Wrap up interaction';
1313
export const WRAP_UP_REASON = 'Wrap-up reason';
1414
export const SELECT = 'Select';
1515
export const SUBMIT_WRAP_UP = 'Submit & Wrap up';
16+
export const MUTE_CALL = 'Mute';
17+
export const UNMUTE_CALL = 'Unmute';
1618

1719
// CallControlCAD constants
1820
export const NO_CUSTOMER_NAME = 'No Customer Name';

packages/contact-center/cc-components/src/components/task/task.types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ export interface ControlProps {
149149
*/
150150
onRecordingToggle?: ({isRecording, task}: {isRecording: boolean; task: ITask}) => void;
151151

152+
/**
153+
* Function to handle mute/unmute toggle actions.
154+
* @param isMuted - Boolean indicating whether the task is muted.
155+
* @param task - The current task being handled.
156+
* @returns void
157+
*/
158+
onToggleMute?: ({isMuted, task}: {isMuted: boolean; task: ITask}) => void;
159+
152160
/**
153161
* Function to handle ending the task.
154162
* @param task - The current task being handled.
@@ -191,6 +199,11 @@ export interface ControlProps {
191199
*/
192200
toggleRecording: () => void;
193201

202+
/**
203+
* Function to handle mute/unmute actions.
204+
*/
205+
toggleMute: () => void;
206+
194207
/**
195208
* Function to handle ending the call.
196209
*/
@@ -230,6 +243,11 @@ export interface ControlProps {
230243
*/
231244
setIsRecording: (isRecording: boolean) => void;
232245

246+
/**
247+
* Flag to determine if the task is muted.
248+
*/
249+
isMuted: boolean;
250+
233251
/**
234252
* List of buddy agents available for consult
235253
*/
@@ -399,6 +417,8 @@ export type CallControlComponentProps = Pick<
399417
| 'wrapupCodes'
400418
| 'toggleHold'
401419
| 'toggleRecording'
420+
| 'toggleMute'
421+
| 'isMuted'
402422
| 'endCall'
403423
| 'wrapupCall'
404424
| 'isHeld'
@@ -496,6 +516,9 @@ export interface CallControlConsultComponentsProps {
496516
isAgentBeingConsulted: boolean;
497517
isEndConsultEnabled: boolean;
498518
logger: ILogger;
519+
muteUnmute: boolean;
520+
isMuted: boolean;
521+
onToggleConsultMute: () => void;
499522
}
500523

501524
/**

packages/contact-center/store/src/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class Store implements IStore {
5151
isEndConsultEnabled: boolean = false;
5252
allowConsultToQueue: boolean = false;
5353
agentProfile: AgentLoginProfile = {};
54+
isMuted: boolean = false;
5455

5556
constructor() {
5657
makeAutoObservable(this, {

packages/contact-center/store/src/store.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ interface IStore {
6969
isEndConsultEnabled: boolean;
7070
allowConsultToQueue: boolean;
7171
agentProfile: AgentLoginProfile;
72+
isMuted: boolean;
7273
init(params: InitParams, callback: (ccSDK: IContactCenter) => void): Promise<void>;
7374
registerCC(webex?: WithWebex['webex']): Promise<void>;
7475
}
@@ -93,6 +94,7 @@ interface IStoreWrapper extends IStore {
9394
setConsultStartTimeStamp(timestamp: number): void;
9495
setAgentProfile(profile: Profile): void;
9596
setTeamId(id: string): void;
97+
setIsMuted(value: boolean): void;
9698
}
9799

98100
interface IWrapupCode {
@@ -125,6 +127,7 @@ enum TASK_EVENTS {
125127
AGENT_CONSULT_CREATED = 'AgentConsultCreated',
126128
TASK_RECORDING_PAUSED = 'task:recordingPaused',
127129
TASK_RECORDING_RESUMED = 'task:recordingResumed',
130+
TASK_OFFER_CONSULT = 'task:offerConsult',
128131
} // TODO: remove this once cc sdk exports this enum
129132

130133
// Events that are received on the contact center SDK

packages/contact-center/store/src/storeEventsWrapper.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ class StoreWrapper implements IStoreWrapper {
149149
return this.store.agentProfile;
150150
}
151151

152+
get isMuted() {
153+
return this.store.isMuted;
154+
}
155+
156+
setIsMuted = (value: boolean): void => {
157+
runInAction(() => {
158+
this.store.isMuted = value;
159+
});
160+
};
161+
152162
setCurrentTheme = (theme: string): void => {
153163
this.store.currentTheme = theme;
154164
};
@@ -356,7 +366,7 @@ class StoreWrapper implements IStoreWrapper {
356366
taskToRemove.off(TASK_EVENTS.TASK_REJECT, (reason) => this.handleTaskReject(taskToRemove, reason));
357367
taskToRemove.off(TASK_EVENTS.AGENT_WRAPPEDUP, this.handleTaskWrapUp);
358368
taskToRemove.off(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting);
359-
taskToRemove.off(CC_EVENTS.AGENT_OFFER_CONSULT, this.handleConsultOffer);
369+
taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer);
360370
taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd);
361371
taskToRemove.off(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted);
362372
taskToRemove.off(TASK_EVENTS.AGENT_CONSULT_CREATED, this.handleConsultCreated);
@@ -386,6 +396,16 @@ class StoreWrapper implements IStoreWrapper {
386396
});
387397
};
388398

399+
handleTaskMuteState = (task: ITask): void => {
400+
const isBrowser = this.deviceType === 'BROWSER';
401+
const webRtcEnabled = this.featureFlags?.webRtcEnabled;
402+
const isTelephony = task?.data?.interaction?.mediaType === 'telephony';
403+
404+
if (isBrowser && isTelephony && webRtcEnabled) {
405+
this.setIsMuted(false);
406+
}
407+
};
408+
389409
handleTaskEnd = () => {
390410
this.refreshTaskList();
391411
};
@@ -497,7 +517,7 @@ class StoreWrapper implements IStoreWrapper {
497517

498518
task.on(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting);
499519
task.on(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted);
500-
task.on(CC_EVENTS.AGENT_OFFER_CONSULT, this.handleConsultOffer);
520+
task.on(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer);
501521
task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd);
502522
task.on(TASK_EVENTS.TASK_HOLD, this.refreshTaskList);
503523
task.on(TASK_EVENTS.TASK_UNHOLD, this.refreshTaskList);
@@ -506,6 +526,7 @@ class StoreWrapper implements IStoreWrapper {
506526
// If it is, we dont have to send the incoming task callback
507527
if (this.onIncomingTask && !this.taskList[task.data.interactionId]) {
508528
this.onIncomingTask({task});
529+
this.handleTaskMuteState(task);
509530
}
510531

511532
// We should update the task list in the store after sending the incoming task callback
@@ -555,7 +576,7 @@ class StoreWrapper implements IStoreWrapper {
555576
task.on(TASK_EVENTS.AGENT_WRAPPEDUP, this.handleTaskWrapUp);
556577

557578
task.on(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting);
558-
task.on(CC_EVENTS.AGENT_OFFER_CONSULT, this.handleConsultOffer);
579+
task.on(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer);
559580
task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd);
560581
task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled);
561582
if (this.deviceType === 'BROWSER') {

packages/contact-center/task/src/CallControl/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {CallControlProps} from '../task.types';
77
import {CallControlComponent} from '@webex/cc-components';
88

99
const CallControl: React.FunctionComponent<CallControlProps> = observer(
10-
({onHoldResume, onEnd, onWrapUp, onRecordingToggle}) => {
10+
({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute}) => {
1111
const {
1212
logger,
1313
currentTask,
@@ -21,6 +21,7 @@ const CallControl: React.FunctionComponent<CallControlProps> = observer(
2121
featureFlags,
2222
isEndConsultEnabled,
2323
allowConsultToQueue,
24+
isMuted,
2425
} = store;
2526

2627
const result = {
@@ -30,10 +31,12 @@ const CallControl: React.FunctionComponent<CallControlProps> = observer(
3031
onEnd,
3132
onWrapUp,
3233
onRecordingToggle,
34+
onToggleMute,
3335
logger,
3436
consultInitiated,
3537
deviceType,
3638
featureFlags,
39+
isMuted,
3740
}),
3841
wrapupCodes,
3942
consultInitiated,

0 commit comments

Comments
 (0)