Files
chatwoot/app/javascript/dashboard/stores/calls.js
T
Tanmay Deep Sharma a5ce1edf20 feat(voice): WhatsApp calling UI — inbox toggle, call widget, badges
Frontend for WhatsApp WABA calling: calling status toggle in inbox settings, WhatsApp Call channel tile, voice badges, call widget redesign, and conversation header call button.
2026-05-20 15:57:05 +05:30

103 lines
3.4 KiB
JavaScript

import { defineStore } from 'pinia';
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
import { cleanupWhatsappSession } from 'dashboard/composables/useWhatsappCallSession';
import { TERMINAL_STATUSES } from 'dashboard/helper/voice';
const teardownByProvider = call => {
if (call?.provider === 'whatsapp') {
cleanupWhatsappSession();
} else {
TwilioVoiceClient.endClientCall();
}
};
export const useCallsStore = defineStore('calls', {
state: () => ({
calls: [],
}),
getters: {
activeCall: state => state.calls.find(call => call.isActive) || null,
hasActiveCall: state => state.calls.some(call => call.isActive),
incomingCalls: state => state.calls.filter(call => !call.isActive),
hasIncomingCall: state => state.calls.some(call => !call.isActive),
},
actions: {
handleCallStatusChanged({ callSid, status }) {
if (!TERMINAL_STATUSES.includes(status)) return;
const call = this.calls.find(c => c.callSid === callSid);
// WhatsApp recordings live in the in-memory recorder until voice_call.ended
// uploads them; tearing down here would race-wipe those chunks.
if (call?.provider === 'whatsapp') {
this.calls = this.calls.filter(c => c.callSid !== callSid);
return;
}
this.removeCall(callSid);
},
addCall(callData) {
if (!callData?.callSid) return;
const existing = this.calls.find(c => c.callSid === callData.callSid);
if (existing) {
// Merge so a later cable event with sdp_offer/provider/caller fills in
// gaps left by the earlier message.created path (and vice versa).
// Preserve a previously-captured caller snapshot when the incoming
// event has no sender info, otherwise the widget would flip to
// "Unknown caller" on the next status update.
const next = { ...callData };
if (existing.caller && !next.caller) delete next.caller;
Object.assign(existing, next, { isActive: existing.isActive });
return;
}
this.calls.push({
...callData,
isActive: false,
});
},
removeCall(callSid) {
const callToRemove = this.calls.find(c => c.callSid === callSid);
if (callToRemove?.isActive) {
teardownByProvider(callToRemove);
}
this.calls = this.calls.filter(c => c.callSid !== callSid);
},
setCallActive(callSid) {
this.calls = this.calls.map(call => ({
...call,
isActive: call.callSid === callSid,
}));
},
clearActiveCall() {
const active = this.calls.find(c => c.isActive);
teardownByProvider(active);
this.calls = this.calls.filter(call => !call.isActive);
},
dismissCall(callSid) {
this.calls = this.calls.filter(call => call.callSid !== callSid);
},
removeCallsForConversation(conversationId) {
const callsToRemove = this.calls.filter(
call => call.conversationId === conversationId
);
// Tear down each active call via its own provider so a WhatsApp call
// gets cleanupWhatsappSession() (closes pc, stops recorder/mic) instead
// of the Twilio-only endClientCall() — otherwise mic stays open.
callsToRemove.filter(call => call.isActive).forEach(teardownByProvider);
this.calls = this.calls.filter(
call => call.conversationId !== conversationId
);
},
},
});