diff --git a/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js b/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js index ec24aae34..d458c1e5d 100644 --- a/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js +++ b/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js @@ -10,10 +10,13 @@ class WhatsappCallsAPI extends ApiClient { return axios.get(`${this.url}/${callId}`).then(r => r.data); } - initiate(conversationId, sdpOffer) { + // Either conversationId, or contactId + inboxId to let the BE resolve the conversation. + initiate({ conversationId, contactId, inboxId }, sdpOffer) { return axios .post(`${this.url}/initiate`, { conversation_id: conversationId, + contact_id: contactId, + inbox_id: inboxId, sdp_offer: sdpOffer, }) .then(r => r.data); diff --git a/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue b/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue index 7e2b6f0c4..5c7ec691d 100644 --- a/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue +++ b/app/javascript/dashboard/components-next/Contacts/VoiceCallButton.vue @@ -16,7 +16,6 @@ import { useAlert } from 'dashboard/composables'; import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper'; import { useCallsStore } from 'dashboard/stores/calls'; import { useWhatsappCallSession } from 'dashboard/composables/useWhatsappCallSession'; -import ContactAPI from 'dashboard/api/contacts'; import Button from 'dashboard/components-next/button/Button.vue'; import Dialog from 'dashboard/components-next/dialog/Dialog.vue'; @@ -83,39 +82,18 @@ const navigateToConversation = conversationId => { const whatsappCallSession = useWhatsappCallSession(); -// Find the most recent open conversation for this contact in the picked inbox. -// WhatsApp /initiate is conversation-scoped (unlike Twilio's contact-scoped path). -// Pass inboxId so the BE applies the filter before the 20-row cap — without it, -// contacts whose latest WhatsApp conversation falls outside the 20 most recent -// across all inboxes would be treated as having no conversation. -const findWhatsappConversationId = async inboxId => { - const { data } = await ContactAPI.getConversations(props.contactId, { - inboxId, - }); - const conversations = data?.payload || []; - const match = [...conversations].sort( - (a, b) => (b.last_activity_at || 0) - (a.last_activity_at || 0) - )[0]; - return match?.id || null; -}; - const startWhatsappCall = async (inboxId, conversationIdHint) => { - // WhatsApp /initiate is conversation-scoped, so we must hand it a - // conversation. Use the caller's hint when given (in-conversation flow); - // otherwise pick the most recent one in the inbox. - const conversationId = - conversationIdHint || (await findWhatsappConversationId(inboxId)); - if (!conversationId) { - useAlert(t('CONTACT_PANEL.CALL_FAILED')); - return; - } - - const response = - await whatsappCallSession.initiateOutboundCall(conversationId); + const response = await whatsappCallSession.initiateOutboundCall( + conversationIdHint + ? { conversationId: conversationIdHint } + : { contactId: props.contactId, inboxId } + ); // The composable returns { status: 'locked' } when an init is already in // flight or a call is already active; treat that as a soft no-op rather than // claiming success. if (response?.status === VOICE_CALL_OUTBOUND_INIT_STATUS.LOCKED) return; + + const conversationId = response?.conversation_id || conversationIdHint; if (!response?.id) { // Permission template path returns no call id. Mirror the header button and // surface whether the request was just sent or is already pending instead of diff --git a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue index 02a00c703..2446e0e2b 100644 --- a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue +++ b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue @@ -234,6 +234,7 @@ onMounted(() => resetContacts()); ref="popoverRef" :align="align" :show-content-border="false" + :close-on-scroll="false" @show="onPopoverShow" @hide="onPopoverHide" > diff --git a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue index 34eb1eaff..1a127d9e6 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue @@ -263,9 +263,9 @@ const handleCallBack = async () => { if (!canCallBack.value || isInitiatingCall.value) return; try { if (isWhatsapp.value) { - const response = await whatsappCallSession.initiateOutboundCall( - conversationId.value - ); + const response = await whatsappCallSession.initiateOutboundCall({ + conversationId: conversationId.value, + }); if (response?.status === VOICE_CALL_OUTBOUND_INIT_STATUS.LOCKED) return; // Permission template path returns no call id — show banner, no widget yet. if (!response?.id) { diff --git a/app/javascript/dashboard/components-next/popover/Popover.vue b/app/javascript/dashboard/components-next/popover/Popover.vue index 5b67e572f..9d369133f 100644 --- a/app/javascript/dashboard/components-next/popover/Popover.vue +++ b/app/javascript/dashboard/components-next/popover/Popover.vue @@ -1,7 +1,11 @@