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:
@@ -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