Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import React, {useState} from 'react';
import {ButtonCircle, TooltipNext, Text} from '@momentum-ui/react-collaboration';
import {Avatar, Icon} from '@momentum-design/components/dist/react';

import {MUTE_CALL, UNMUTE_CALL} from '../../constants';
import TaskTimer from '../../TaskTimer';
import {CallControlConsultComponentsProps} from '../../task.types';

Expand All @@ -14,7 +14,12 @@ const CallControlConsultComponent: React.FC<CallControlConsultComponentsProps> =
isAgentBeingConsulted,
isEndConsultEnabled,
logger,
muteUnmute,
isMuted,
onToggleConsultMute,
}) => {
const [isMuteDisabled, setIsMuteDisabled] = useState(false);

const timerKey = `timer-${startTimeStamp}`;

const handleTransfer = () => {
Expand Down Expand Up @@ -51,7 +56,35 @@ const CallControlConsultComponent: React.FC<CallControlConsultComponentsProps> =
}
};

const handleConsultMuteToggle = () => {
setIsMuteDisabled(true);

try {
onToggleConsultMute();
} catch (error) {
logger.error('Mute toggle failed:', {
error,
module: 'call-control-consult.tsx',
method: 'handleConsultMuteToggle',
});
} finally {
// Re-enable button after operation
setTimeout(() => {
setIsMuteDisabled(false);
}, 500);
}
};

const buttons = [
{
key: 'mute',
icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold',
onClick: handleConsultMuteToggle,
tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL,
className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`,
disabled: isMuteDisabled,
shouldShow: muteUnmute,
},
{
key: 'transfer',
icon: 'next-bold',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,37 @@
--mdc-icon-fill-color: var(--mds-color-theme-button-cancel-normal);
}

.md-button-circle-wrapper.call-control-button-muted {
width: 1rem;
height: 1rem;
min-width: 2rem;
min-height: 2rem;
padding: 0.5rem;
border-radius: 50%;
background-color: var(--mds-color-theme-button-secondary-normal);
border: 1px solid var(--mds-color-theme-outline-cancel-normal);
&:hover,
&.hover {
background-color: var(--mds-color-theme-button-secondary-hover);
}
.call-control-button-muted-icon {
--mdc-icon-fill-color: var(--mds-color-theme-text-error-normal);
}
}

.md-button-circle-wrapper.call-control-button-muted[disabled],
.md-button-circle-wrapper.call-control-button-muted.shallowDisabled,
.md-button-circle-wrapper.call-control-button-muted:disabled {
outline-color: var(--mds-color-theme-button-primary-disabled);
border-color: var(--mds-color-theme-button-primary-disabled);
opacity: 0.5;
cursor: not-allowed;
.call-control-button-muted-icon {
--mdc-icon-fill-color: var(--mds--color-theme-button-secondary-normal);
}
}


.wrapup-button {
display: inline-flex;
justify-content: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,23 @@ import {
WRAP_UP_REASON,
SELECT,
SUBMIT_WRAP_UP,
MUTE_CALL,
UNMUTE_CALL,
} from '../constants';

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

const {
currentTask,
toggleHold,
toggleRecording,
toggleMute,
isMuted,
endCall,
wrapupCall,
wrapupCodes,
Expand Down Expand Up @@ -82,6 +87,25 @@ function CallControlComponent(props: CallControlComponentProps) {
setIsHeld(!isHeld);
};

const handleMuteToggle = () => {
setIsMuteButtonDisabled(true);

try {
toggleMute();
} catch (error) {
logger.error('Mute toggle failed:', {
error,
module: 'call-control.tsx',
method: 'handleMuteToggle',
});
} finally {
// Re-enable button after operation
setTimeout(() => {
setIsMuteButtonDisabled(false);
}, 500);
}
};

const handleWrapupCall = () => {
logger.info('CC-Widgets: CallControl: wrap-up submitted', {
module: 'call-control.tsx',
Expand Down Expand Up @@ -151,6 +175,15 @@ function CallControlComponent(props: CallControlComponentProps) {
const isTelephony = mediaType === 'telephony';

const buttons = [
{
id: 'mute',
icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold',
onClick: handleMuteToggle,
tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL,
className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`,
disabled: isMuteButtonDisabled,
isVisible: controlVisibility.muteUnmute,
},
{
id: 'hold',
icon: isHeld ? 'play-bold' : 'pause-bold',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const CallControlCADComponent: React.FC<CallControlComponentProps> = (props) =>
lastTargetType,
controlVisibility,
logger,
isMuted,
toggleMute,
} = props;

const formatTime = (time: number): string => {
Expand Down Expand Up @@ -207,6 +209,9 @@ const CallControlCADComponent: React.FC<CallControlComponentProps> = (props) =>
isAgentBeingConsulted={!consultAccepted}
isEndConsultEnabled={isEndConsultEnabled}
logger={logger}
muteUnmute={controlVisibility.muteUnmute}
isMuted={isMuted}
onToggleConsultMute={toggleMute}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const WRAP_UP_INTERACTION = 'Wrap up interaction';
export const WRAP_UP_REASON = 'Wrap-up reason';
export const SELECT = 'Select';
export const SUBMIT_WRAP_UP = 'Submit & Wrap up';
export const MUTE_CALL = 'Mute';
export const UNMUTE_CALL = 'Unmute';

// CallControlCAD constants
export const NO_CUSTOMER_NAME = 'No Customer Name';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ export interface ControlProps {
*/
onRecordingToggle?: ({isRecording, task}: {isRecording: boolean; task: ITask}) => void;

/**
* Function to handle mute/unmute toggle actions.
* @param isMuted - Boolean indicating whether the task is muted.
* @param task - The current task being handled.
* @returns void
*/
onToggleMute?: ({isMuted, task}: {isMuted: boolean; task: ITask}) => void;

/**
* Function to handle ending the task.
* @param task - The current task being handled.
Expand Down Expand Up @@ -191,6 +199,11 @@ export interface ControlProps {
*/
toggleRecording: () => void;

/**
* Function to handle mute/unmute actions.
*/
toggleMute: () => void;

/**
* Function to handle ending the call.
*/
Expand Down Expand Up @@ -230,6 +243,11 @@ export interface ControlProps {
*/
setIsRecording: (isRecording: boolean) => void;

/**
* Flag to determine if the task is muted.
*/
isMuted: boolean;

/**
* List of buddy agents available for consult
*/
Expand Down Expand Up @@ -399,6 +417,8 @@ export type CallControlComponentProps = Pick<
| 'wrapupCodes'
| 'toggleHold'
| 'toggleRecording'
| 'toggleMute'
| 'isMuted'
| 'endCall'
| 'wrapupCall'
| 'isHeld'
Expand Down Expand Up @@ -496,6 +516,9 @@ export interface CallControlConsultComponentsProps {
isAgentBeingConsulted: boolean;
isEndConsultEnabled: boolean;
logger: ILogger;
muteUnmute: boolean;
isMuted: boolean;
onToggleConsultMute: () => void;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/contact-center/store/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class Store implements IStore {
isEndConsultEnabled: boolean = false;
allowConsultToQueue: boolean = false;
agentProfile: AgentLoginProfile = {};
isMuted: boolean = false;

constructor() {
makeAutoObservable(this, {
Expand Down
3 changes: 3 additions & 0 deletions packages/contact-center/store/src/store.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ interface IStore {
isEndConsultEnabled: boolean;
allowConsultToQueue: boolean;
agentProfile: AgentLoginProfile;
isMuted: boolean;
init(params: InitParams, callback: (ccSDK: IContactCenter) => void): Promise<void>;
registerCC(webex?: WithWebex['webex']): Promise<void>;
}
Expand All @@ -93,6 +94,7 @@ interface IStoreWrapper extends IStore {
setConsultStartTimeStamp(timestamp: number): void;
setAgentProfile(profile: Profile): void;
setTeamId(id: string): void;
setIsMuted(value: boolean): void;
}

interface IWrapupCode {
Expand Down Expand Up @@ -125,6 +127,7 @@ enum TASK_EVENTS {
AGENT_CONSULT_CREATED = 'AgentConsultCreated',
TASK_RECORDING_PAUSED = 'task:recordingPaused',
TASK_RECORDING_RESUMED = 'task:recordingResumed',
TASK_OFFER_CONSULT = 'task:offerConsult',
} // TODO: remove this once cc sdk exports this enum

// Events that are received on the contact center SDK
Expand Down
27 changes: 24 additions & 3 deletions packages/contact-center/store/src/storeEventsWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ class StoreWrapper implements IStoreWrapper {
return this.store.agentProfile;
}

get isMuted() {
return this.store.isMuted;
}

setIsMuted = (value: boolean): void => {
runInAction(() => {
this.store.isMuted = value;
});
};

setCurrentTheme = (theme: string): void => {
this.store.currentTheme = theme;
};
Expand Down Expand Up @@ -356,7 +366,7 @@ class StoreWrapper implements IStoreWrapper {
taskToRemove.off(TASK_EVENTS.TASK_REJECT, (reason) => this.handleTaskReject(taskToRemove, reason));
taskToRemove.off(TASK_EVENTS.AGENT_WRAPPEDUP, this.handleTaskWrapUp);
taskToRemove.off(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting);
taskToRemove.off(CC_EVENTS.AGENT_OFFER_CONSULT, this.handleConsultOffer);
taskToRemove.off(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer);
taskToRemove.off(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd);
taskToRemove.off(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted);
taskToRemove.off(TASK_EVENTS.AGENT_CONSULT_CREATED, this.handleConsultCreated);
Expand Down Expand Up @@ -386,6 +396,16 @@ class StoreWrapper implements IStoreWrapper {
});
};

handleTaskMuteState = (task: ITask): void => {
const isBrowser = this.deviceType === 'BROWSER';
const webRtcEnabled = this.featureFlags?.webRtcEnabled;
const isTelephony = task?.data?.interaction?.mediaType === 'telephony';

if (isBrowser && isTelephony && webRtcEnabled) {
this.setIsMuted(false);
}
};

handleTaskEnd = () => {
this.refreshTaskList();
};
Expand Down Expand Up @@ -497,7 +517,7 @@ class StoreWrapper implements IStoreWrapper {

task.on(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting);
task.on(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this.handleConsultAccepted);
task.on(CC_EVENTS.AGENT_OFFER_CONSULT, this.handleConsultOffer);
task.on(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer);
task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd);
task.on(TASK_EVENTS.TASK_HOLD, this.refreshTaskList);
task.on(TASK_EVENTS.TASK_UNHOLD, this.refreshTaskList);
Expand All @@ -506,6 +526,7 @@ class StoreWrapper implements IStoreWrapper {
// If it is, we dont have to send the incoming task callback
if (this.onIncomingTask && !this.taskList[task.data.interactionId]) {
this.onIncomingTask({task});
this.handleTaskMuteState(task);
}

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

task.on(TASK_EVENTS.TASK_CONSULTING, this.handleConsulting);
task.on(CC_EVENTS.AGENT_OFFER_CONSULT, this.handleConsultOffer);
task.on(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer);
task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd);
task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled);
if (this.deviceType === 'BROWSER') {
Expand Down
5 changes: 4 additions & 1 deletion packages/contact-center/task/src/CallControl/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {CallControlProps} from '../task.types';
import {CallControlComponent} from '@webex/cc-components';

const CallControl: React.FunctionComponent<CallControlProps> = observer(
({onHoldResume, onEnd, onWrapUp, onRecordingToggle}) => {
({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute}) => {
const {
logger,
currentTask,
Expand All @@ -21,6 +21,7 @@ const CallControl: React.FunctionComponent<CallControlProps> = observer(
featureFlags,
isEndConsultEnabled,
allowConsultToQueue,
isMuted,
} = store;

const result = {
Expand All @@ -30,10 +31,12 @@ const CallControl: React.FunctionComponent<CallControlProps> = observer(
onEnd,
onWrapUp,
onRecordingToggle,
onToggleMute,
logger,
consultInitiated,
deviceType,
featureFlags,
isMuted,
}),
wrapupCodes,
consultInitiated,
Expand Down
Loading
Loading