fix(voice): accept calls without conversation hydration; warn on refresh during ringing
- FloatingCallWidget: drop !conversation early-return in handleJoinCall — on a fresh account or after a hard refresh the inbound call's conversation may not be in the Vuex store yet, which silently no-op'd Accept while Reject worked (Reject doesn't read the conversation). - useCallSession: seed the calls store from already-loaded voice_call messages with status='ringing' on mount and whenever the conversation list changes. Cable events (voice_call.incoming, message.created) are one-shot and not replayed on reconnect, so without seeding a refresh during a ringing call leaves the FloatingCallWidget empty. - useCallSession: extend the beforeunload warning to fire while a call is ringing too — losing a ringing inbound to refresh is the same UX hit as losing an active one.
This commit is contained in:
@@ -74,21 +74,25 @@ const handleEndCall = async () => {
|
||||
};
|
||||
|
||||
const handleJoinCall = async call => {
|
||||
if (!call || isJoining.value) return;
|
||||
const { conversation } = getCallInfo(call);
|
||||
if (!call || !conversation || isJoining.value) return;
|
||||
|
||||
// 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.
|
||||
const result = await joinCall({
|
||||
conversationId: call.conversationId,
|
||||
inboxId: conversation.inbox_id,
|
||||
inboxId: call.inboxId || conversation?.inbox_id,
|
||||
callSid: call.callSid,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
if (result && conversation) {
|
||||
router.push({
|
||||
name: 'inbox_conversation',
|
||||
params: { conversation_id: call.conversationId },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { computed, ref, watch, onUnmounted, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import VoiceAPI from 'dashboard/api/channel/voice/voiceAPIClient';
|
||||
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
@@ -7,11 +8,13 @@ import {
|
||||
sendWhatsappTerminateBeacon,
|
||||
cleanupWhatsappSession,
|
||||
} from 'dashboard/composables/useWhatsappCallSession';
|
||||
import { handleVoiceCallCreated } from 'dashboard/helper/voice';
|
||||
import Timer from 'dashboard/helper/Timer';
|
||||
|
||||
const isWhatsappCall = call => call?.provider === 'whatsapp';
|
||||
|
||||
export function useCallSession() {
|
||||
const store = useStore();
|
||||
const callsStore = useCallsStore();
|
||||
const whatsappSession = useWhatsappCallSession();
|
||||
const isJoining = ref(false);
|
||||
@@ -23,6 +26,7 @@ export function useCallSession() {
|
||||
const activeCall = computed(() => callsStore.activeCall);
|
||||
const incomingCalls = computed(() => callsStore.incomingCalls);
|
||||
const hasActiveCall = computed(() => callsStore.hasActiveCall);
|
||||
const hasIncomingCall = computed(() => callsStore.hasIncomingCall);
|
||||
|
||||
watch(
|
||||
hasActiveCall,
|
||||
@@ -39,13 +43,32 @@ export function useCallSession() {
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
const handleBeforeUnload = event => {
|
||||
if (!hasActiveCall.value) return;
|
||||
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.
|
||||
const seedCallsFromHydratedMessages = () => {
|
||||
const conversations = store.getters.getAllConversations || [];
|
||||
const currentUserId = store.getters.getCurrentUserID;
|
||||
conversations.forEach(conv => {
|
||||
(conv.messages || []).forEach(msg => {
|
||||
if (msg.content_type !== 'voice_call') return;
|
||||
if (msg.call?.status !== 'ringing') return;
|
||||
handleVoiceCallCreated(msg, currentUserId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 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 = () => {
|
||||
@@ -61,8 +84,17 @@ export function useCallSession() {
|
||||
);
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
window.addEventListener('pagehide', handlePageHide);
|
||||
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).
|
||||
watch(
|
||||
() => store.getters.getAllConversations?.length,
|
||||
() => seedCallsFromHydratedMessages()
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
durationTimer.stop();
|
||||
TwilioVoiceClient.removeEventListener(
|
||||
|
||||
Reference in New Issue
Block a user