mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-13 21:01:16 +08:00
168 lines
4.8 KiB
JavaScript
168 lines
4.8 KiB
JavaScript
import { computed, ref, watch, onUnmounted, onMounted } from 'vue';
|
|
import VoiceAPI from 'dashboard/api/channel/voice/voiceAPIClient';
|
|
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
|
|
import { useCallsStore } from 'dashboard/stores/calls';
|
|
import {
|
|
useWhatsappCallSession,
|
|
sendWhatsappTerminateBeacon,
|
|
} from 'dashboard/composables/useWhatsappCallSession';
|
|
import Timer from 'dashboard/helper/Timer';
|
|
|
|
const isWhatsappCall = call => call?.provider === 'whatsapp';
|
|
|
|
export function useCallSession() {
|
|
const callsStore = useCallsStore();
|
|
const whatsappSession = useWhatsappCallSession();
|
|
const isJoining = ref(false);
|
|
const callDuration = ref(0);
|
|
const durationTimer = new Timer(elapsed => {
|
|
callDuration.value = elapsed;
|
|
});
|
|
|
|
const activeCall = computed(() => callsStore.activeCall);
|
|
const incomingCalls = computed(() => callsStore.incomingCalls);
|
|
const hasActiveCall = computed(() => callsStore.hasActiveCall);
|
|
|
|
watch(
|
|
hasActiveCall,
|
|
active => {
|
|
if (active) {
|
|
durationTimer.start();
|
|
} else {
|
|
durationTimer.stop();
|
|
callDuration.value = 0;
|
|
}
|
|
},
|
|
{ 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, so warn either way.
|
|
const handleBeforeUnload = event => {
|
|
if (!hasActiveCall.value) return;
|
|
event.preventDefault();
|
|
event.returnValue = '';
|
|
};
|
|
|
|
// pagehide fires after the user confirms the prompt. Let the WhatsApp session
|
|
// best-effort sendBeacon a terminate so the server doesn't keep the call open.
|
|
const handlePageHide = () => {
|
|
sendWhatsappTerminateBeacon();
|
|
};
|
|
|
|
const handleTwilioDisconnected = () => callsStore.clearActiveCall();
|
|
|
|
onMounted(() => {
|
|
TwilioVoiceClient.addEventListener(
|
|
'call:disconnected',
|
|
handleTwilioDisconnected
|
|
);
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
window.addEventListener('pagehide', handlePageHide);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
durationTimer.stop();
|
|
TwilioVoiceClient.removeEventListener(
|
|
'call:disconnected',
|
|
handleTwilioDisconnected
|
|
);
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
window.removeEventListener('pagehide', handlePageHide);
|
|
});
|
|
|
|
const findCall = callSid => callsStore.calls.find(c => c.callSid === callSid);
|
|
|
|
const endCall = async ({ conversationId, inboxId, callSid }) => {
|
|
if (isWhatsappCall(findCall(callSid))) {
|
|
await whatsappSession.endActiveCall();
|
|
durationTimer.stop();
|
|
callsStore.clearActiveCall();
|
|
return;
|
|
}
|
|
|
|
await VoiceAPI.leaveConference({ inboxId, conversationId, callSid });
|
|
TwilioVoiceClient.endClientCall();
|
|
durationTimer.stop();
|
|
callsStore.clearActiveCall();
|
|
};
|
|
|
|
const joinCall = async ({ conversationId, inboxId, callSid }) => {
|
|
if (isJoining.value) return null;
|
|
|
|
isJoining.value = true;
|
|
try {
|
|
const call = findCall(callSid);
|
|
if (isWhatsappCall(call)) {
|
|
await whatsappSession.acceptIncomingCall({
|
|
callId: call.callId,
|
|
sdpOffer: call.sdpOffer,
|
|
iceServers: call.iceServers,
|
|
});
|
|
callsStore.setCallActive(callSid);
|
|
durationTimer.start();
|
|
return { callId: call.callId };
|
|
}
|
|
|
|
const device = await TwilioVoiceClient.initializeDevice(inboxId);
|
|
if (!device) return null;
|
|
|
|
const joinResponse = await VoiceAPI.joinConference({
|
|
conversationId,
|
|
inboxId,
|
|
callSid,
|
|
});
|
|
|
|
await TwilioVoiceClient.joinClientCall({
|
|
to: joinResponse?.conference_sid,
|
|
conversationId,
|
|
callSid,
|
|
});
|
|
|
|
callsStore.setCallActive(callSid);
|
|
durationTimer.start();
|
|
|
|
return { conferenceSid: joinResponse?.conference_sid };
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('Failed to join call:', error);
|
|
return null;
|
|
} finally {
|
|
isJoining.value = false;
|
|
}
|
|
};
|
|
|
|
const rejectIncomingCall = callSid => {
|
|
const call = findCall(callSid);
|
|
if (isWhatsappCall(call) && call?.callId) {
|
|
whatsappSession.rejectIncomingCall(call.callId);
|
|
} else {
|
|
TwilioVoiceClient.endClientCall();
|
|
}
|
|
callsStore.dismissCall(callSid);
|
|
};
|
|
|
|
const dismissCall = callSid => {
|
|
callsStore.dismissCall(callSid);
|
|
};
|
|
|
|
const formattedCallDuration = computed(() => {
|
|
const minutes = Math.floor(callDuration.value / 60);
|
|
const seconds = callDuration.value % 60;
|
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
});
|
|
|
|
return {
|
|
activeCall,
|
|
incomingCalls,
|
|
hasActiveCall,
|
|
isJoining,
|
|
formattedCallDuration,
|
|
joinCall,
|
|
endCall,
|
|
rejectIncomingCall,
|
|
dismissCall,
|
|
};
|
|
}
|