From 0e87519ecdf7f9e22d21697308ddce5b5e5e1132 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:59:23 +0530 Subject: [PATCH] feat: add mute button for Twilio calls (#14637) ## Linear Ticket https://linear.app/chatwoot/issue/CW-7264/add-mute-button-in-twilio-calls ## Description Adds mute support for Twilio voice calls. Previously the mute button was shown only for WhatsApp calls (which toggle the local mic track in the browser), so Twilio calls had no way to mute. The call widget now routes mute by provider: WhatsApp continues to toggle the local mic track, while Twilio uses the Voice SDK connection's native `mute()`. The mute button is now shown for any active call, and unmute works symmetrically. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../api/channel/voice/twilioVoiceClient.js | 6 ++++++ .../components-next/call/FloatingCallWidget.vue | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/javascript/dashboard/api/channel/voice/twilioVoiceClient.js b/app/javascript/dashboard/api/channel/voice/twilioVoiceClient.js index 13f61a16c..14dd56ec9 100644 --- a/app/javascript/dashboard/api/channel/voice/twilioVoiceClient.js +++ b/app/javascript/dashboard/api/channel/voice/twilioVoiceClient.js @@ -48,6 +48,12 @@ class TwilioVoiceClient extends EventTarget { return !!this.activeConnection; } + setMuted(shouldMute) { + if (!this.activeConnection) return false; + this.activeConnection.mute(shouldMute); + return shouldMute; + } + endClientCall() { if (this.activeConnection) { this.activeConnection.disconnect(); diff --git a/app/javascript/dashboard/components-next/call/FloatingCallWidget.vue b/app/javascript/dashboard/components-next/call/FloatingCallWidget.vue index 86d9e979c..f8dfabb35 100644 --- a/app/javascript/dashboard/components-next/call/FloatingCallWidget.vue +++ b/app/javascript/dashboard/components-next/call/FloatingCallWidget.vue @@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'; import { useStore } from 'vuex'; import { useCallSession } from 'dashboard/composables/useCallSession'; import { setWhatsappCallMuted } from 'dashboard/composables/useWhatsappCallSession'; +import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient'; import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper'; import { VOICE_CALL_PROVIDERS } from 'dashboard/helper/inbox'; import { VOICE_CALL_DIRECTION } from 'dashboard/components-next/message/constants'; @@ -29,8 +30,8 @@ const { formattedCallDuration, } = useCallSession(); -// Mute is currently WhatsApp-only — Twilio calls are mediated server-side and -// don't expose a mic track on the browser side. +// Mute routes by provider: WhatsApp toggles the local mic track, Twilio uses +// the Voice SDK connection's native mute. Both surface the same button. const isMuted = ref(false); const isWhatsappActive = computed( () => activeCall.value?.provider === VOICE_CALL_PROVIDERS.WHATSAPP @@ -63,7 +64,11 @@ const stackedCardState = call => const toggleMute = () => { isMuted.value = !isMuted.value; - setWhatsappCallMuted(isMuted.value); + if (isWhatsappActive.value) { + setWhatsappCallMuted(isMuted.value); + } else { + TwilioVoiceClient.setMuted(isMuted.value); + } }; watch(hasActiveCall, active => { @@ -256,7 +261,7 @@ onBeforeUnmount(stopRingtone); :call-info="getCallInfo(activeCall || primaryIncomingCall)" :duration="hasActiveCall ? formattedCallDuration : ''" :is-muted="isMuted" - :show-mute="hasActiveCall && isWhatsappActive" + :show-mute="hasActiveCall" @accept="handleJoinCall(primaryIncomingCall)" @reject="rejectIncomingCall(primaryIncomingCall?.callSid)" @dismiss="dismissCall(primaryIncomingCall?.callSid)"