feat(voice): WhatsApp Cloud Calling — UI
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { useCallSession } from 'dashboard/composables/useCallSession';
|
||||
import { setWhatsappCallMuted } from 'dashboard/composables/useWhatsappCallSession';
|
||||
import WindowVisibilityHelper from 'dashboard/helper/AudioAlerts/WindowVisibilityHelper';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
@@ -21,16 +22,40 @@ const {
|
||||
formattedCallDuration,
|
||||
} = useCallSession();
|
||||
|
||||
// Mute is currently WhatsApp-only — Twilio calls are mediated server-side and
|
||||
// don't expose a mic track on the browser side.
|
||||
const isMuted = ref(false);
|
||||
const isWhatsappActive = computed(
|
||||
() => activeCall.value?.provider === 'whatsapp'
|
||||
);
|
||||
|
||||
const toggleMute = () => {
|
||||
isMuted.value = !isMuted.value;
|
||||
setWhatsappCallMuted(isMuted.value);
|
||||
};
|
||||
|
||||
watch(hasActiveCall, active => {
|
||||
if (!active) isMuted.value = false;
|
||||
});
|
||||
|
||||
const getCallInfo = call => {
|
||||
const conversation = store.getters.getConversationById(call?.conversationId);
|
||||
const inbox = store.getters['inboxes/getInbox'](conversation?.inbox_id);
|
||||
const sender = conversation?.meta?.sender;
|
||||
// Inbound WhatsApp calls stash caller info on the call record (from the cable
|
||||
// payload) so the widget has something to show before the conversation lands.
|
||||
const caller = call?.caller;
|
||||
return {
|
||||
conversation,
|
||||
inbox,
|
||||
contactName: sender?.name || sender?.phone_number || 'Unknown caller',
|
||||
contactName:
|
||||
sender?.name ||
|
||||
sender?.phone_number ||
|
||||
caller?.name ||
|
||||
caller?.phone ||
|
||||
'Unknown caller',
|
||||
inboxName: inbox?.name || 'Customer support',
|
||||
avatar: sender?.avatar || sender?.thumbnail,
|
||||
avatar: sender?.avatar || sender?.thumbnail || caller?.avatar,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -162,6 +187,30 @@ watch(
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<button
|
||||
v-if="hasActiveCall && isWhatsappActive"
|
||||
v-tooltip.top="
|
||||
isMuted
|
||||
? $t('CONVERSATION.VOICE_WIDGET.UNMUTE')
|
||||
: $t('CONVERSATION.VOICE_WIDGET.MUTE')
|
||||
"
|
||||
class="flex justify-center items-center w-10 h-10 rounded-full transition-colors"
|
||||
:class="
|
||||
isMuted
|
||||
? 'bg-n-amber-9 hover:bg-n-amber-10'
|
||||
: 'bg-n-slate-3 hover:bg-n-slate-4'
|
||||
"
|
||||
@click="toggleMute"
|
||||
>
|
||||
<i
|
||||
class="text-lg"
|
||||
:class="
|
||||
isMuted
|
||||
? 'text-white i-ph-microphone-slash-bold'
|
||||
: 'text-n-slate-12 i-ph-microphone-bold'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="flex justify-center items-center w-10 h-10 bg-n-ruby-9 hover:bg-n-ruby-10 rounded-full transition-colors"
|
||||
@click="
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import BackButton from '../BackButton.vue';
|
||||
import ButtonV4 from 'dashboard/components-next/button/Button.vue';
|
||||
import InboxName from '../InboxName.vue';
|
||||
import MoreActions from './MoreActions.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
@@ -12,6 +13,13 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { useInbox } from 'dashboard/composables/useInbox';
|
||||
import {
|
||||
getVoiceCallProvider,
|
||||
VOICE_CALL_PROVIDERS,
|
||||
} from 'dashboard/helper/inbox';
|
||||
import { useWhatsappCallSession } from 'dashboard/composables/useWhatsappCallSession';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -91,6 +99,45 @@ const hasMultipleInboxes = computed(
|
||||
);
|
||||
|
||||
const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
||||
|
||||
const callsStore = useCallsStore();
|
||||
const whatsappCallSession = useWhatsappCallSession();
|
||||
|
||||
const isWhatsappVoiceInbox = computed(
|
||||
() => getVoiceCallProvider(inbox.value) === VOICE_CALL_PROVIDERS.WHATSAPP
|
||||
);
|
||||
|
||||
const startWhatsappCall = async () => {
|
||||
if (whatsappCallSession.isInitiating.value) return;
|
||||
try {
|
||||
const response = await whatsappCallSession.initiateOutboundCall(
|
||||
currentChat.value.id
|
||||
);
|
||||
|
||||
// Permission template path returns no call id — show banner, no widget yet.
|
||||
if (!response?.id) {
|
||||
const status = response?.status;
|
||||
const messageKey =
|
||||
status === 'permission_pending'
|
||||
? 'CONVERSATION.HEADER.WHATSAPP_CALL_PERMISSION_PENDING'
|
||||
: 'CONVERSATION.HEADER.WHATSAPP_CALL_PERMISSION_REQUESTED';
|
||||
useAlert(t(messageKey));
|
||||
return;
|
||||
}
|
||||
|
||||
callsStore.addCall({
|
||||
callSid: response.call_id,
|
||||
callId: response.id,
|
||||
conversationId: currentChat.value.id,
|
||||
inboxId: inbox.value?.id,
|
||||
callDirection: 'outbound',
|
||||
provider: 'whatsapp',
|
||||
});
|
||||
callsStore.setCallActive(response.call_id);
|
||||
} catch (error) {
|
||||
useAlert(error?.message || t('CONVERSATION.HEADER.WHATSAPP_CALL_FAILED'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -152,6 +199,18 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
||||
:parent-width="width"
|
||||
class="hidden md:flex"
|
||||
/>
|
||||
<ButtonV4
|
||||
v-if="isWhatsappVoiceInbox"
|
||||
v-tooltip.bottom="$t('CONVERSATION.HEADER.WHATSAPP_CALL')"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
icon="i-lucide-phone"
|
||||
:is-loading="whatsappCallSession.isInitiating.value"
|
||||
:disabled="whatsappCallSession.isInitiating.value"
|
||||
class="rounded-md hover:bg-n-alpha-2"
|
||||
@click="startWhatsappCall"
|
||||
/>
|
||||
<MoreActions :conversation-id="currentChat.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user