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.
This commit is contained in:
Tanmay Deep Sharma
2026-05-20 15:57:05 +05:30
parent 0ef7585ccb
commit a5ce1edf20
18 changed files with 350 additions and 94 deletions
@@ -14,10 +14,6 @@ import { handleVoiceCallCreated } from 'dashboard/helper/voice';
import Timer from 'dashboard/helper/Timer';
const isWhatsappCall = call => call?.provider === 'whatsapp';
// Calls seeded after a refresh / arriving via message.updated may lack provider
// metadata. Treat anything that has a callId (Meta's wacid) as WhatsApp — the
// Twilio path keys off provider_call_id (a CA…) and never sets callId.
const isWhatsappLikeCall = call => isWhatsappCall(call) || !!call?.callId;
// Dismissed call sids must not be re-seeded by the conversation-load watcher.
// Lives at module scope so all consumers share the same set.
@@ -86,7 +82,7 @@ const buildCallActions = ({ callsStore, whatsappSession, t }) => {
const endCall = async ({ conversationId, inboxId, callSid }) => {
const call = findCall(callSid);
if (isWhatsappLikeCall(call)) {
if (isWhatsappCall(call)) {
// Pass call.callId so a wiped module state (e.g. a prior accept attempt
// tore down the WebRTC session) doesn't stop us hitting /terminate.
await whatsappSession.endActiveCall(call?.callId);
@@ -111,13 +107,13 @@ const buildCallActions = ({ callsStore, whatsappSession, t }) => {
// cleanup() and destroy the live outbound session. Outbound *Twilio*
// calls still need joinConference + joinClientCall (FloatingCallWidget
// auto-joins them), so don't short-circuit those.
if (call?.callDirection === 'outbound' && isWhatsappLikeCall(call)) {
if (call?.callDirection === 'outbound' && isWhatsappCall(call)) {
return null;
}
globalIsJoining.value = true;
try {
if (isWhatsappLikeCall(call)) {
if (isWhatsappCall(call)) {
await whatsappSession.acceptIncomingCall({
callId: call.callId,
sdpOffer: call.sdpOffer,
@@ -170,7 +166,7 @@ const buildCallActions = ({ callsStore, whatsappSession, t }) => {
const rejectIncomingCall = async callSid => {
const call = findCall(callSid);
try {
if (isWhatsappLikeCall(call) && call?.callId) {
if (isWhatsappCall(call) && call?.callId) {
if (call.callDirection === 'outbound') {
// Outbound calls that are still ringing must be terminated, not
// rejected (reject is the inbound-side verb on Meta's API).
@@ -178,6 +174,15 @@ const buildCallActions = ({ callsStore, whatsappSession, t }) => {
} else {
await whatsappSession.rejectIncomingCall(call.callId);
}
} else if (call?.inboxId && call?.conversationId) {
// Twilio incoming reject: agent hasn't joined the Device yet, so
// endClientCall is a no-op. End the conference server-side instead
// so Twilio hangs up the inbound leg.
await VoiceAPI.leaveConference({
inboxId: call.inboxId,
conversationId: call.conversationId,
callSid,
});
} else {
TwilioVoiceClient.endClientCall();
}
@@ -234,13 +239,14 @@ export function useCallSession() {
const seedCallsFromHydratedMessages = () => {
const conversations = store.getters.getAllConversations || [];
const currentUserId = store.getters.getCurrentUserID;
const currentUserAvailability = store.getters.getCurrentUserAvailability;
conversations.forEach(conv => {
(conv.messages || []).forEach(msg => {
if (msg.content_type !== 'voice_call') return;
if (msg.call?.status !== 'ringing') return;
const callSid = msg.call?.provider_call_id;
if (callSid && dismissedCallSids.has(callSid)) return;
handleVoiceCallCreated(msg, currentUserId);
handleVoiceCallCreated(msg, currentUserId, currentUserAvailability);
});
});
};