From 0b1ad1a85400a2ac0dcc15b9132c6ae0e8af8d3a Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Mon, 4 May 2026 15:16:04 +0700 Subject: [PATCH] chore(voice): trim dead exports, fix duplicate-OR bug, drop redundant comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branch review pass: - useWhatsappCallSession: drop unused `error` ref and unused exports (hasActiveWhatsappCall, isWhatsappCallMuted), fold sendWhatsappCallBeacon into a private beaconTerminate helper since only sendWhatsappTerminateBeacon consumed it externally. Trim WHAT-comments; keep WHY-comments (browser-quirk explanations, race-condition notes, auth-cookie rationale). - VoiceCall.vue bubble: fix `data || data` short-circuit that did nothing — upstream key transform was the same on both branches; collapse to one read. - calls/useCallSession/actionCable/FloatingCallWidget/VoiceCallButton: drop comments that just describe what the next line already says. Net diff: -63 lines across 7 files. No behavior change. --- .../Contacts/VoiceCallButton.vue | 2 - .../message/bubbles/VoiceCall.vue | 11 +- .../components/widgets/FloatingCallWidget.vue | 7 +- .../dashboard/composables/useCallSession.js | 32 ++-- .../composables/useWhatsappCallSession.js | 168 +++++++----------- .../dashboard/helper/actionCable.js | 11 +- app/javascript/dashboard/stores/calls.js | 8 +- 7 files changed, 88 insertions(+), 151 deletions(-) diff --git a/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue b/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue index 65d31baeb..2ffbac7ff 100644 --- a/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue +++ b/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue @@ -44,7 +44,6 @@ const voiceInboxes = computed(() => ); const hasVoiceInboxes = computed(() => voiceInboxes.value.length > 0); -// Unified behavior: hide when no phone const shouldRender = computed(() => hasVoiceInboxes.value && !!props.phone); const isInitiatingCall = computed(() => { @@ -128,7 +127,6 @@ const startCall = async inboxId => { }); const { call_sid: callSid, conversation_id: conversationId } = response; - // Add call to store immediately so widget shows const callsStore = useCallsStore(); callsStore.addCall({ callSid, diff --git a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue index bcf97b707..809b9b164 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue @@ -43,15 +43,14 @@ const audioAttachment = computed(() => (attachments?.value || []).find(a => a.fileType === 'audio') ); -// Duration lives in two places depending on which payload the FE got: -// - call.duration_seconds / call.durationSeconds (push_event_data shape) -// - content_attributes.data.duration_seconds (message-side mirror) -// Both can be camelCased by useTransformKeys upstream — check every variant. +// Duration may arrive on call.duration_seconds (push_event_data) or +// content_attributes.data.duration_seconds — and either may be camelCased +// upstream, so check every variant. const durationSeconds = computed(() => { const fromCall = call.value?.durationSeconds || call.value?.duration_seconds; if (fromCall != null) return fromCall; - const data = contentAttributes?.value?.data || contentAttributes?.value?.data; + const data = contentAttributes?.value?.data; return data?.durationSeconds || data?.duration_seconds; }); @@ -118,8 +117,6 @@ const bgColor = computed(() => BG_COLOR_MAP[status.value] || 'bg-n-teal-9'); {{ $t(labelKey) }} - {{ audioAttachment ? $t(subtextKey) diff --git a/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue b/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue index a8f4326b1..fe22eb1eb 100644 --- a/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue +++ b/app/javascript/dashboard/components/widgets/FloatingCallWidget.vue @@ -77,15 +77,12 @@ const handleJoinCall = async call => { if (!call || isJoining.value) return; const { conversation } = getCallInfo(call); - // End current active call before joining new one if (hasActiveCall.value) { await handleEndCall(); } - // After a hard refresh the conversation may not be hydrated yet — but the call - // object already carries inboxId from the cable / seeding path, so accept can - // proceed without it. Twilio still needs inboxId for initializeDevice; falls - // back to the conversation's inbox_id when present. + // The conversation may not be hydrated yet (post-refresh seeding path); + // call.inboxId already carries what joinCall needs. const result = await joinCall({ conversationId: call.conversationId, inboxId: call.inboxId || conversation?.inbox_id, diff --git a/app/javascript/dashboard/composables/useCallSession.js b/app/javascript/dashboard/composables/useCallSession.js index 97191bdde..5cba13569 100644 --- a/app/javascript/dashboard/composables/useCallSession.js +++ b/app/javascript/dashboard/composables/useCallSession.js @@ -41,22 +41,19 @@ export function useCallSession() { { immediate: true } ); - // Browser-native confirm prompt when reload/close happens mid-call. Reload - // tears down the WebRTC session permanently for WhatsApp (no rejoin) and - // drops the agent leg for Twilio. Also warn while a call is ringing — the - // cable broadcast that delivered the incoming-call event isn't replayed on - // refresh, so the agent loses the ability to accept it. + // Warn before a refresh/close drops a live or ringing call. Cable events + // aren't replayed on reconnect, so a confirmed refresh during ringing would + // leave the agent unable to accept; for active calls the WebRTC session + // dies outright (no rejoin path). const handleBeforeUnload = event => { if (!hasActiveCall.value && !hasIncomingCall.value) return; event.preventDefault(); event.returnValue = ''; }; - // Hydrate the calls store from already-loaded conversation messages. The - // voice_call.incoming / message.created cable events are one-shot and aren't - // replayed when the page reconnects, so without this seeding a hard refresh - // during a ringing call would leave the FloatingCallWidget empty even though - // the call is still ringing on Meta's side. + // Cable broadcasts (voice_call.incoming / message.created) are one-shot, so + // on a hard refresh they leave the calls store empty. Seed it from any + // ringing voice_call message in the conversation cache. const seedCallsFromHydratedMessages = () => { const conversations = store.getters.getAllConversations || []; const currentUserId = store.getters.getCurrentUserID; @@ -69,11 +66,8 @@ export function useCallSession() { }); }; - // pagehide fires after the user confirms the refresh prompt. Terminate the - // active call only — its WebRTC session dies with the page and can't be - // rejoined. Ringing calls intentionally stay alive on Meta so the agent can - // pick them up after the page reloads (FloatingCallWidget rehydrates them - // via seedCallsFromHydratedMessages once the conversation messages land). + // Terminate only the active call — ringing calls stay alive on Meta so the + // agent can pick them up after reload (seeded back via the watcher above). const handlePageHide = () => { sendWhatsappTerminateBeacon(); }; @@ -90,9 +84,7 @@ export function useCallSession() { seedCallsFromHydratedMessages(); }); - // Conversations are typically fetched after this composable mounts, so the - // initial seed pass runs before any messages exist. Re-seed whenever the - // conversation list changes — addCall is idempotent (it merges by callSid). + // Re-seed when conversations stream in after mount; addCall merges by callSid. watch( () => store.getters.getAllConversations?.length, () => seedCallsFromHydratedMessages() @@ -163,9 +155,7 @@ export function useCallSession() { } catch (error) { // eslint-disable-next-line no-console console.error('Failed to join call:', error); - // Tear down any half-built WebRTC state so the user's next click starts - // fresh; otherwise the leftover pc + mic stream survives and confuses - // the second-attempt SDP exchange. + // Drop any half-built WebRTC state so the next click starts fresh. cleanupWhatsappSession(); return null; } finally { diff --git a/app/javascript/dashboard/composables/useWhatsappCallSession.js b/app/javascript/dashboard/composables/useWhatsappCallSession.js index 3faf73aa5..2985caac6 100644 --- a/app/javascript/dashboard/composables/useWhatsappCallSession.js +++ b/app/javascript/dashboard/composables/useWhatsappCallSession.js @@ -2,9 +2,8 @@ import { ref } from 'vue'; import Cookies from 'js-cookie'; import WhatsappCallsAPI from 'dashboard/api/channel/whatsapp/whatsappCallsAPI'; -// Browser ↔ Meta WebRTC is a singleton — only one PeerConnection at a time can -// hold the user's mic. Module-level state lets cable handlers and the pagehide -// listener reach the live session without prop-drilling refs through composables. +// Module-level state lets the cable handlers and unload listeners reach the +// live PeerConnection without prop-drilling refs through every composable. let pc = null; let localStream = null; let remoteStream = null; @@ -15,9 +14,6 @@ let audioContext = null; let activeCallId = null; let intentionallyClosing = false; -// Lazily attach a hidden