fix: initating Call at app level and showing minimized call on sidebar
Handled incoming call UI and outgoing call UI
This commit is contained in:
parent
3868f5dbe5
commit
50e5fae2b7
@ -1,7 +1,7 @@
|
||||
from werkzeug.wrappers import Response
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from twilio.rest import Client
|
||||
from .twilio_handler import Twilio, IncomingCall
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -58,3 +58,24 @@ def twilio_incoming_call_handler(**kwargs):
|
||||
|
||||
resp = IncomingCall(args.From, args.To).process()
|
||||
return Response(resp.to_xml(), mimetype='text/xml')
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_call_info(**kwargs):
|
||||
"""This is a webhook called when the outgoing call status changes.
|
||||
E.g. 'initiated' 'ringing', 'in-progress', 'completed' etc.
|
||||
"""
|
||||
args = frappe._dict(kwargs)
|
||||
call_info = {
|
||||
'ParentCallSid': args.ParentCallSid,
|
||||
'CallSid': args.CallSid,
|
||||
'CallStatus': args.CallStatus,
|
||||
'CallDuration': args.CallDuration,
|
||||
'From': args.From,
|
||||
'To': args.To,
|
||||
}
|
||||
|
||||
client = Twilio.get_twilio_client()
|
||||
client.calls(args.ParentCallSid).user_defined_messages.create(
|
||||
content=json.dumps(call_info)
|
||||
)
|
||||
@ -70,8 +70,8 @@ class Twilio:
|
||||
"""
|
||||
return identity.replace('(at)', '@')
|
||||
|
||||
def get_recording_status_callback_url(self):
|
||||
url_path = "/api/method/twilio_integration.twilio_integration.api.update_recording_info"
|
||||
def get_call_status_callback_url(self):
|
||||
url_path = "/api/method/crm.twilio.api.get_call_info"
|
||||
return get_public_url(url_path)
|
||||
|
||||
def generate_twilio_dial_response(self, from_number: str, to_number: str):
|
||||
@ -84,7 +84,12 @@ class Twilio:
|
||||
# recording_status_callback=self.get_recording_status_callback_url(),
|
||||
# recording_status_callback_event='completed'
|
||||
)
|
||||
dial.number(to_number)
|
||||
dial.number(
|
||||
to_number,
|
||||
status_callback_event='initiated ringing answered completed',
|
||||
status_callback=self.get_call_status_callback_url(),
|
||||
status_callback_method='POST'
|
||||
)
|
||||
resp.append(dial)
|
||||
return resp
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ from frappe.utils import get_url
|
||||
|
||||
|
||||
def get_public_url(path: str=None):
|
||||
return get_url(path)
|
||||
return get_url().split(":8", 1)[0] + path
|
||||
|
||||
|
||||
def merge_dicts(d1: dict, d2: dict):
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<router-view v-if="$route.name == 'Login'" />
|
||||
<DesktopLayout v-else-if="session().isLoggedIn">
|
||||
<router-view />
|
||||
</DesktopLayout>
|
||||
<CallUI v-else-if="session().isLoggedIn">
|
||||
<DesktopLayout>
|
||||
<router-view />
|
||||
</DesktopLayout>
|
||||
</CallUI>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DesktopLayout from '@/components/DesktopLayout.vue'
|
||||
import { sessionStore as session } from '@/stores/session'
|
||||
import CallUI from './components/CallUI.vue'
|
||||
</script>
|
||||
|
||||
@ -1,25 +1,33 @@
|
||||
<template>
|
||||
<div class="flex p-2">
|
||||
<UserDropdown />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<nav class="space-y-0.5 px-2">
|
||||
<NavLinks
|
||||
:links="navigations"
|
||||
class="flex items-center rounded px-2 py-1 text-gray-800 transition-all duration-300 ease-in-out"
|
||||
active="bg-white shadow-sm"
|
||||
inactive="hover:bg-gray-100"
|
||||
>
|
||||
<template v-slot="{ link }">
|
||||
<div class="flex w-full items-center space-x-2">
|
||||
<span class="grid h-5 w-6 place-items-center">
|
||||
<component :is="link.icon" class="h-4.5 w-4.5 text-gray-700" />
|
||||
</span>
|
||||
<span class="text-base">{{ link.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</NavLinks>
|
||||
</nav>
|
||||
<div class="flex flex-col h-full justify-between">
|
||||
<div>
|
||||
<div class="flex p-2">
|
||||
<UserDropdown />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<nav class="space-y-0.5 px-2">
|
||||
<NavLinks
|
||||
:links="navigations"
|
||||
class="flex items-center rounded px-2 py-1 text-gray-800 transition-all duration-300 ease-in-out"
|
||||
active="bg-white shadow-sm"
|
||||
inactive="hover:bg-gray-100"
|
||||
>
|
||||
<template v-slot="{ link }">
|
||||
<div class="flex w-full items-center space-x-2">
|
||||
<span class="grid h-5 w-6 place-items-center">
|
||||
<component
|
||||
:is="link.icon"
|
||||
class="h-4.5 w-4.5 text-gray-700"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-base">{{ link.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</NavLinks>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div id="call-area"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
435
frontend/src/components/CallUI.vue
Normal file
435
frontend/src/components/CallUI.vue
Normal file
@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<slot />
|
||||
<Dialog
|
||||
v-model="showPhoneCall"
|
||||
:options="{
|
||||
title: 'Make a call...',
|
||||
actions: [{ label: 'Make a call...', variant: 'solid' }],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div>Make a call to +917666980887</div>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="flex flex-row-reverse gap-2">
|
||||
<Button
|
||||
variant="solid"
|
||||
label="Make a call..."
|
||||
@click="makeOutgoingCall(close)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
<div
|
||||
v-if="showCallPopup"
|
||||
ref="callPopup"
|
||||
class="fixed select-none z-10 bg-gray-900 rounded-lg shadow-lg p-4 flex flex-col w-60"
|
||||
:style="style"
|
||||
>
|
||||
<div class="flex items-center flex-row-reverse gap-1">
|
||||
<DragIcon1 ref="callPopopHandle" class="w-4 h-4 cursor-move text-white" />
|
||||
<MinimizeIcon
|
||||
class="w-4 h-4 text-white cursor-pointer"
|
||||
@click="toggleCallWindow"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center gap-2">
|
||||
<UserAvatar
|
||||
:user="getUser().name"
|
||||
class="flex items-center justify-center !h-24 !w-24 relative"
|
||||
:class="onCall || calling ? '' : 'pulse'"
|
||||
/>
|
||||
<div class="text-xl font-medium text-white">
|
||||
{{ getUser().full_name }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">+917666980887</div>
|
||||
<div class="text-white text-base my-1">
|
||||
{{
|
||||
onCall
|
||||
? '0:38'
|
||||
: callStatus == 'ringing'
|
||||
? 'Ringing...'
|
||||
: callStatus == 'initiated' || callStatus == 'calling'
|
||||
? 'Calling...'
|
||||
: 'Incoming call...'
|
||||
}}
|
||||
</div>
|
||||
<div v-if="onCall" class="flex gap-2">
|
||||
<Button
|
||||
:icon="muted ? 'mic-off' : 'mic'"
|
||||
class="rounded-full"
|
||||
@click="toggleMute"
|
||||
/>
|
||||
<Button class="rounded-full">
|
||||
<template #icon>
|
||||
<DialpadIcon class="rounded-full cursor-pointer" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button class="rounded-full bg-red-600 hover:bg-red-700">
|
||||
<template #icon>
|
||||
<PhoneIcon
|
||||
class="text-white fill-white h-4 w-4 rotate-[135deg]"
|
||||
@click="hangUpCall"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else-if="calling">
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
theme="red"
|
||||
label="Cancel"
|
||||
@click="cancelCall"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneIcon class="text-white fill-white h-4 w-4 rotate-[135deg]" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="flex gap-2 text-sm mt-2">
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
theme="green"
|
||||
label="Accept"
|
||||
@click="acceptIncomingCall"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneIcon class="text-white fill-white h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
theme="red"
|
||||
label="Reject"
|
||||
@click="rejectIncomingCall"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneIcon class="text-white fill-white h-4 w-4 rotate-[135deg]" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Teleport v-if="showSmallCallWindow" to="#call-area">
|
||||
<div
|
||||
class="flex items-center justify-between p-1.5 gap-2 bg-gray-900 rounded m-2 cursor-pointer select-none"
|
||||
@click="toggleCallWindow"
|
||||
>
|
||||
<div class="inline-flex items-center gap-1.5 truncate">
|
||||
<UserAvatar
|
||||
:user="getUser().name"
|
||||
class="flex items-center justify-center"
|
||||
/>
|
||||
<div class="text-base font-medium text-white truncate">
|
||||
Shariq Ansari
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="onCall" class="flex items-center gap-1.5">
|
||||
<div class="text-white text-base my-1">0:38</div>
|
||||
<Button variant="solid" theme="red" class="rounded-full !h-6 !w-6">
|
||||
<template #icon>
|
||||
<PhoneIcon
|
||||
class="text-white fill-white h-3 w-3 rotate-[135deg]"
|
||||
@click.stop="hangUpCall"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else-if="calling" class="flex items-center gap-1.5">
|
||||
<div class="text-white text-base my-1">
|
||||
{{ callStatus == 'ringing' ? 'Ringing...' : 'Calling...' }}
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
theme="red"
|
||||
class="rounded-full !h-6 !w-6"
|
||||
@click.stop="cancelCall"
|
||||
>
|
||||
<template #icon>
|
||||
<PhoneIcon class="text-white fill-white h-3 w-3 rotate-[135deg]" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="flex gap-1.5 text-sm">
|
||||
<Button
|
||||
variant="solid"
|
||||
theme="green"
|
||||
class="rounded-full !h-6 !w-6 pulse relative"
|
||||
@click.stop="acceptIncomingCall"
|
||||
>
|
||||
<template #icon>
|
||||
<PhoneIcon class="text-white fill-white h-3 w-3 animate-pulse" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
theme="red"
|
||||
class="rounded-full !h-6 !w-6"
|
||||
@click.stop="rejectIncomingCall"
|
||||
>
|
||||
<template #icon>
|
||||
<PhoneIcon class="text-white fill-white h-3 w-3 rotate-[135deg]" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DragIcon1 from '@/components/Icons/DragIcon1.vue'
|
||||
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
||||
import DialpadIcon from '@/components/Icons/DialpadIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { Device } from '@twilio/voice-sdk'
|
||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { call } from 'frappe-ui'
|
||||
import { onMounted, provide, ref, watch } from 'vue'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
let device = ''
|
||||
let log = ref('Connecting...')
|
||||
let _call = ref(null)
|
||||
let showPhoneCall = ref(false)
|
||||
|
||||
let showCallPopup = ref(false)
|
||||
let showSmallCallWindow = ref(false)
|
||||
let onCall = ref(false)
|
||||
let muted = ref(false)
|
||||
let callPopup = ref(null)
|
||||
let callPopopHandle = ref(null)
|
||||
let calling = ref(false)
|
||||
|
||||
const { width, height } = useWindowSize()
|
||||
|
||||
let { style } = useDraggable(callPopup, {
|
||||
initialValue: { x: width.value - 280, y: height.value - 310 },
|
||||
handle: callPopopHandle,
|
||||
preventDefault: true,
|
||||
})
|
||||
|
||||
async function startupClient() {
|
||||
log.value = 'Requesting Access Token...'
|
||||
|
||||
try {
|
||||
const data = await call('crm.twilio.api.generate_access_token')
|
||||
log.value = 'Got a token.'
|
||||
intitializeDevice(data.token)
|
||||
} catch (err) {
|
||||
log.value = 'An error occurred. ' + err.message
|
||||
}
|
||||
}
|
||||
|
||||
function intitializeDevice(token) {
|
||||
device = new Device(token, {
|
||||
codecPreferences: ['opus', 'pcmu'],
|
||||
fakeLocalDTMF: true,
|
||||
enableRingingState: true,
|
||||
})
|
||||
|
||||
addDeviceListeners()
|
||||
|
||||
device.register()
|
||||
}
|
||||
|
||||
function addDeviceListeners() {
|
||||
device.on('registered', () => {
|
||||
log.value = 'Ready to make and receive calls!'
|
||||
})
|
||||
|
||||
device.on('unregistered', (device) => {
|
||||
log.value = 'Logged out'
|
||||
})
|
||||
|
||||
device.on('error', (error) => {
|
||||
log.value = 'Twilio.Device Error: ' + error.message
|
||||
})
|
||||
|
||||
device.on('incoming', handleIncomingCall)
|
||||
|
||||
device.on('connect', (conn) => {
|
||||
conn
|
||||
debugger
|
||||
log.value = 'Successfully established call!'
|
||||
})
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (_call.value.isMuted()) {
|
||||
_call.value.mute(false)
|
||||
muted.value = false
|
||||
} else {
|
||||
_call.value.mute()
|
||||
muted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleIncomingCall(call) {
|
||||
log.value = `Incoming call from ${call.parameters.From}`
|
||||
|
||||
showCallPopup.value = true
|
||||
_call.value = call
|
||||
|
||||
// add event listener to call object
|
||||
call.on('cancel', handleDisconnectedIncomingCall)
|
||||
call.on('disconnect', handleDisconnectedIncomingCall)
|
||||
call.on('reject', handleDisconnectedIncomingCall)
|
||||
}
|
||||
|
||||
function acceptIncomingCall() {
|
||||
_call.value.accept()
|
||||
|
||||
log.value = 'Accepted incoming call.'
|
||||
onCall.value = true
|
||||
}
|
||||
|
||||
function rejectIncomingCall() {
|
||||
_call.value.reject()
|
||||
log.value = 'Rejected incoming call'
|
||||
showCallPopup.value = false
|
||||
callStatus.value = ''
|
||||
}
|
||||
|
||||
function hangUpCall() {
|
||||
_call.value.disconnect()
|
||||
log.value = 'Hanging up incoming call'
|
||||
onCall.value = false
|
||||
callStatus.value = ''
|
||||
}
|
||||
|
||||
function handleDisconnectedIncomingCall() {
|
||||
log.value = `Call ended.`
|
||||
showCallPopup.value = false
|
||||
_call.value = null
|
||||
}
|
||||
|
||||
let callStatus = ref('')
|
||||
|
||||
async function makeOutgoingCall(close) {
|
||||
close()
|
||||
if (device) {
|
||||
log.value = `Attempting to call +917666980887 ...`
|
||||
|
||||
try {
|
||||
_call.value = await device.connect({
|
||||
params: {
|
||||
To: '+917666980887',
|
||||
},
|
||||
})
|
||||
|
||||
_call.value.on('messageReceived', (message) => {
|
||||
let info = message.content
|
||||
callStatus.value = info.CallStatus
|
||||
|
||||
log.value = `Call status: ${info.CallStatus}`
|
||||
|
||||
if (info.CallStatus == 'in-progress') {
|
||||
log.value = `Call in progress.`
|
||||
showCallPopup.value = true
|
||||
calling.value = false
|
||||
onCall.value = true
|
||||
}
|
||||
})
|
||||
|
||||
_call.value.on('accept', () => {
|
||||
log.value = `Initiated call!`
|
||||
showCallPopup.value = true
|
||||
calling.value = true
|
||||
onCall.value = false
|
||||
callStatus.value = 'calling'
|
||||
})
|
||||
_call.value.on('disconnect', () => {
|
||||
log.value = `Call ended.`
|
||||
calling.value = false
|
||||
onCall.value = false
|
||||
showCallPopup.value = false
|
||||
_call.value = null
|
||||
callStatus.value = ''
|
||||
})
|
||||
_call.value.on('cancel', () => {
|
||||
log.value = `Call ended.`
|
||||
calling.value = false
|
||||
onCall.value = false
|
||||
showCallPopup.value = false
|
||||
_call.value = null
|
||||
callStatus.value = ''
|
||||
})
|
||||
} catch (error) {
|
||||
log.value = `Could not connect call: ${error.message}`
|
||||
}
|
||||
} else {
|
||||
log.value = 'Unable to make call.'
|
||||
}
|
||||
}
|
||||
|
||||
function cancelCall() {
|
||||
showCallPopup.value = false
|
||||
calling.value = false
|
||||
onCall.value = false
|
||||
callStatus.value = ''
|
||||
_call.value.disconnect()
|
||||
}
|
||||
|
||||
function toggleCallWindow() {
|
||||
showCallPopup.value = !showCallPopup.value
|
||||
showSmallCallWindow.value = !showSmallCallWindow.value
|
||||
}
|
||||
|
||||
onMounted(() => startupClient())
|
||||
|
||||
watch(
|
||||
() => log.value,
|
||||
(value) => {
|
||||
console.log(value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
provide('showPhoneCall', showPhoneCall)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pulse::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 1px solid green;
|
||||
width: calc(100% + 20px);
|
||||
height: calc(100% + 20px);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s linear infinite;
|
||||
}
|
||||
|
||||
.pulse::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 1px solid green;
|
||||
width: calc(100% + 20px);
|
||||
height: calc(100% + 20px);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s linear infinite;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.3);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -10,7 +10,7 @@
|
||||
<Tooltip text="Make a call..." class="m-1">
|
||||
<PhoneIcon
|
||||
class="bg-gray-900 rounded-full text-white fill-white p-[3px]"
|
||||
@click.stop="openPhoneCallDialog"
|
||||
@click.stop="showPhoneCall = true"
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
@ -47,78 +47,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog
|
||||
v-model="showPhoneCall"
|
||||
:options="{
|
||||
title: 'Make a call...',
|
||||
actions: [{ label: 'Make a call...', variant: 'solid' }],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div>Make a call to +917666980887</div>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="flex flex-row-reverse gap-2">
|
||||
<Button
|
||||
variant="solid"
|
||||
label="Make a call..."
|
||||
@click="makeOutgoingCall"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
<div
|
||||
v-show="showIncomingCall"
|
||||
ref="incomingCallPopup"
|
||||
class="fixed select-none z-10 bg-white rounded-lg shadow-lg p-4 flex flex-col gap-4 w-60"
|
||||
:style="style"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>Incoming Call...</div>
|
||||
<DragIcon ref="incomingCallHandle" class="w-4 h-4 cursor-move" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center gap-2">
|
||||
<UserAvatar
|
||||
:user="getUser().name"
|
||||
class="flex items-center justify-center !h-24 !w-24 relative pulse"
|
||||
/>
|
||||
<div class="text-xl font-medium">{{ getUser().full_name }}</div>
|
||||
<div class="text-sm text-gray-500">+917666980887</div>
|
||||
<div v-if="onCall" class="flex gap-2">
|
||||
<Button :icon="muted ? 'mic-off' : 'mic'" @click="toggleMute" />
|
||||
<Button
|
||||
variant="solid"
|
||||
theme="red"
|
||||
icon="phone-off"
|
||||
@click="rejectIncomingCall"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex gap-2 text-sm mt-2">
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
theme="green"
|
||||
label="Accept"
|
||||
@click="acceptIncomingCall"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneIcon class="text-white fill-white h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
theme="red"
|
||||
label="Reject"
|
||||
@click="rejectIncomingCall"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneIcon class="text-white fill-white h-4 w-4 rotate-[135deg]" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -126,16 +54,15 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import EmailEditor from '@/components/EmailEditor.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { Tooltip, Dialog, call } from 'frappe-ui'
|
||||
import { ref, watch, computed, defineModel } from 'vue'
|
||||
import { Device } from '@twilio/voice-sdk'
|
||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||
import DragIcon from '@/components/Icons/DragIcon.vue'
|
||||
import { Tooltip, call, Button } from 'frappe-ui'
|
||||
import { ref, watch, computed, defineModel, inject } from 'vue'
|
||||
|
||||
const modelValue = defineModel()
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
let showPhoneCall = inject('showPhoneCall')
|
||||
|
||||
const showCommunicationBox = ref(false)
|
||||
const newEmail = ref('')
|
||||
const newEmailEditor = ref(null)
|
||||
@ -180,178 +107,7 @@ async function submitComment() {
|
||||
modelValue.value.reload()
|
||||
}
|
||||
|
||||
let device = ''
|
||||
let log = ref('Connecting...')
|
||||
let incomingCall = ref(null)
|
||||
let showPhoneCall = ref(false)
|
||||
|
||||
let showIncomingCall = ref(false)
|
||||
let onCall = ref(false)
|
||||
let muted = ref(false)
|
||||
let incomingCallPopup = ref(null)
|
||||
let incomingCallHandle = ref(null)
|
||||
|
||||
const { width, height } = useWindowSize()
|
||||
|
||||
let { style } = useDraggable(incomingCallPopup, {
|
||||
initialValue: { x: width.value - 280, y: height.value - 300 },
|
||||
handle: incomingCallHandle,
|
||||
preventDefault: true,
|
||||
})
|
||||
|
||||
function openPhoneCallDialog() {
|
||||
showPhoneCall.value = true
|
||||
startupClient()
|
||||
//
|
||||
}
|
||||
|
||||
async function startupClient() {
|
||||
log.value = 'Requesting Access Token...'
|
||||
|
||||
try {
|
||||
const data = await call('crm.twilio.api.generate_access_token')
|
||||
log.value = 'Got a token.'
|
||||
intitializeDevice(data.token)
|
||||
} catch (err) {
|
||||
log.value = 'An error occurred. ' + err.message
|
||||
}
|
||||
}
|
||||
|
||||
function intitializeDevice(token) {
|
||||
device = new Device(token, {
|
||||
codecPreferences: ['opus', 'pcmu'],
|
||||
fakeLocalDTMF: true,
|
||||
enableRingingState: true,
|
||||
})
|
||||
|
||||
addDeviceListeners()
|
||||
|
||||
device.register()
|
||||
}
|
||||
|
||||
function addDeviceListeners() {
|
||||
device.on('registered', () => {
|
||||
log.value = 'Ready to make and receive calls!'
|
||||
})
|
||||
|
||||
device.on('unregistered', (device) => {
|
||||
log.value = 'Logged out'
|
||||
})
|
||||
|
||||
device.on('error', (error) => {
|
||||
log.value = 'Twilio.Device Error: ' + error.message
|
||||
})
|
||||
|
||||
device.on('incoming', handleIncomingCall)
|
||||
|
||||
device.on('connect', (conn) => {
|
||||
log.value = 'Successfully established call!'
|
||||
})
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (incomingCall.value.isMuted()) {
|
||||
incomingCall.value.mute(false)
|
||||
muted.value = false
|
||||
} else {
|
||||
incomingCall.value.mute()
|
||||
muted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleIncomingCall(call) {
|
||||
log.value = `Incoming call from ${call.parameters.From}`
|
||||
|
||||
showIncomingCall.value = true
|
||||
incomingCall.value = call
|
||||
|
||||
// add event listener to call object
|
||||
call.on('cancel', handleDisconnectedIncomingCall)
|
||||
call.on('disconnect', handleDisconnectedIncomingCall)
|
||||
call.on('reject', handleDisconnectedIncomingCall)
|
||||
}
|
||||
|
||||
function acceptIncomingCall() {
|
||||
incomingCall.value.accept()
|
||||
|
||||
log.value = 'Accepted incoming call.'
|
||||
onCall.value = true
|
||||
}
|
||||
|
||||
function rejectIncomingCall() {
|
||||
incomingCall.value.reject()
|
||||
log.value = 'Rejected incoming call'
|
||||
showIncomingCall.value = false
|
||||
}
|
||||
|
||||
function handleDisconnectedIncomingCall() {
|
||||
log.value = `Call ended.`
|
||||
showIncomingCall.value = false
|
||||
incomingCall.value = null
|
||||
}
|
||||
|
||||
async function makeOutgoingCall() {
|
||||
if (device) {
|
||||
log.value = `Attempting to call +917666980887 ...`
|
||||
|
||||
try {
|
||||
const call = await device.connect({
|
||||
params: {
|
||||
To: '+917666980887',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
log.value = `Could not connect call: ${error.message}`
|
||||
}
|
||||
} else {
|
||||
log.value = 'Unable to make call.'
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => log.value,
|
||||
(value) => {
|
||||
console.log(value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pulse::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 1px solid green;
|
||||
width: calc(100% + 20px);
|
||||
height: calc(100% + 20px);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s linear infinite;
|
||||
}
|
||||
|
||||
.pulse::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 1px solid green;
|
||||
width: calc(100% + 20px);
|
||||
height: calc(100% + 20px);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s linear infinite;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.3);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
73
frontend/src/components/Icons/DialpadIcon.vue
Normal file
73
frontend/src/components/Icons/DialpadIcon.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4.00002C8.36819 4.00002 8.66667 3.70154 8.66667 3.33335C8.66667 2.96516 8.36819 2.66669 8 2.66669C7.63181 2.66669 7.33334 2.96516 7.33334 3.33335C7.33334 3.70154 7.63181 4.00002 8 4.00002Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.6667 4.00002C13.0349 4.00002 13.3333 3.70154 13.3333 3.33335C13.3333 2.96516 13.0349 2.66669 12.6667 2.66669C12.2985 2.66669 12 2.96516 12 3.33335C12 3.70154 12.2985 4.00002 12.6667 4.00002Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.33333 4.00002C3.70152 4.00002 4 3.70154 4 3.33335C4 2.96516 3.70152 2.66669 3.33333 2.66669C2.96514 2.66669 2.66666 2.96516 2.66666 3.33335C2.66666 3.70154 2.96514 4.00002 3.33333 4.00002Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 8.66665C8.36819 8.66665 8.66667 8.36817 8.66667 7.99998C8.66667 7.63179 8.36819 7.33331 8 7.33331C7.63181 7.33331 7.33334 7.63179 7.33334 7.99998C7.33334 8.36817 7.63181 8.66665 8 8.66665Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.6667 8.66665C13.0349 8.66665 13.3333 8.36817 13.3333 7.99998C13.3333 7.63179 13.0349 7.33331 12.6667 7.33331C12.2985 7.33331 12 7.63179 12 7.99998C12 8.36817 12.2985 8.66665 12.6667 8.66665Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.33333 8.66665C3.70152 8.66665 4 8.36817 4 7.99998C4 7.63179 3.70152 7.33331 3.33333 7.33331C2.96514 7.33331 2.66666 7.63179 2.66666 7.99998C2.66666 8.36817 2.96514 8.66665 3.33333 8.66665Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 13.3333C8.36819 13.3333 8.66667 13.0349 8.66667 12.6667C8.66667 12.2985 8.36819 12 8 12C7.63181 12 7.33334 12.2985 7.33334 12.6667C7.33334 13.0349 7.63181 13.3333 8 13.3333Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.6667 13.3333C13.0349 13.3333 13.3333 13.0349 13.3333 12.6667C13.3333 12.2985 13.0349 12 12.6667 12C12.2985 12 12 12.2985 12 12.6667C12 13.0349 12.2985 13.3333 12.6667 13.3333Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.33333 13.3333C3.70152 13.3333 4 13.0349 4 12.6667C4 12.2985 3.70152 12 3.33333 12C2.96514 12 2.66666 12.2985 2.66666 12.6667C2.66666 13.0349 2.96514 13.3333 3.33333 13.3333Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
16
frontend/src/components/Icons/DragIcon1.vue
Normal file
16
frontend/src/components/Icons/DragIcon1.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.25 13C9.25 12.3096 9.80964 11.75 10.5 11.75C11.1904 11.75 11.75 12.3096 11.75 13C11.75 13.6904 11.1904 14.25 10.5 14.25C9.80964 14.25 9.25 13.6904 9.25 13ZM9.25 3C9.25 2.30964 9.80964 1.75 10.5 1.75C11.1904 1.75 11.75 2.30964 11.75 3C11.75 3.69036 11.1904 4.25 10.5 4.25C9.80964 4.25 9.25 3.69036 9.25 3ZM10.5 6.75C9.80964 6.75 9.25 7.30964 9.25 8C9.25 8.69036 9.80964 9.25 10.5 9.25C11.1904 9.25 11.75 8.69036 11.75 8C11.75 7.30964 11.1904 6.75 10.5 6.75ZM4.25 13C4.25 12.3096 4.8096 11.75 5.5 11.75C6.19036 11.75 6.75 12.3096 6.75 13C6.75 13.6904 6.19036 14.25 5.5 14.25C4.8096 14.25 4.25 13.6904 4.25 13ZM5.5 1.75C4.8096 1.75 4.25 2.30964 4.25 3C4.25 3.69036 4.8096 4.25 5.5 4.25C6.19036 4.25 6.75 3.69036 6.75 3C6.75 2.30964 6.19036 1.75 5.5 1.75ZM4.25 8C4.25 7.30964 4.8096 6.75 5.5 6.75C6.19036 6.75 6.75 7.30964 6.75 8C6.75 8.69036 6.19036 9.25 5.5 9.25C4.8096 9.25 4.25 8.69036 4.25 8Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
24
frontend/src/components/Icons/MinimizeIcon.vue
Normal file
24
frontend/src/components/Icons/MinimizeIcon.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.66667 9.33331H6.66667V13.3333"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.3333 6.66669H9.33333V2.66669"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -9,7 +9,7 @@ readme = "README.md"
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
# "frappe~=15.0.0" # Installed and managed by bench.
|
||||
"twilio==6.44.2"
|
||||
"twilio==8.5.0"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user