TT#32756 Call: As a Customer, I want to input numbers via a Dialpad

to be able to send DTMF

Change-Id: I96efb4b24f2758e31f80e9489b0938930a35bfce
changes/47/22847/3
Hans-Peter Herzog 7 years ago
parent f867c69d26
commit fc6e8c616c

@ -1,92 +1,296 @@
<template>
<div class="csc-call">
<audio ref="incomingSound" loop preload="auto" src="statics/ring.mp3"></audio>
<q-card flat color="secondary">
<div :class="callClasses">
<audio
ref="incomingSound"
loop
preload="auto"
src="statics/ring.mp3"
></audio>
<q-card
flat
color="secondary"
>
<q-card-title>
<span v-if="isRinging || isInitiating || isIncoming">
<q-spinner-rings color="primary" :size="50" />
<q-spinner-rings
color="primary"
:size="50"
/>
</span>
<q-icon v-if="isPreparing" name="call" color="primary" size="26px"/>
<q-icon v-if="isEstablished && isCaller" name="call made" color="primary" size="26px"/>
<q-icon v-if="isEstablished && isCallee" name="call received" color="primary" size="26px"/>
<q-icon v-if="isEnded" name="error" color="primary" size="26px"/>
<q-icon
v-if="isPreparing"
name="call"
color="primary"
size="26px"
/>
<q-icon
v-if="isEstablished && isCaller"
name="call made"
color="primary"
size="26px"
/>
<q-icon
v-if="isEstablished && isCallee"
name="call received"
color="primary"
size="26px"
/>
<q-icon
v-if="isEnded"
name="error"
color="primary"
size="26px"
/>
<span v-if="isPreparing" class="text">{{ $t('call.startNew') }}</span>
<span v-else-if="isInitiating" class="text">{{ $t('call.initiating') }}</span>
<span v-else-if="isRinging" class="text">{{ $t('call.ringing') }}</span>
<span v-else-if="isEnded" class="text">{{ $t('call.ended') }}</span>
<span v-else-if="isIncoming" class="text">{{ $t('call.incoming') }}</span>
<span v-else class="text">{{ $t('call.call') }}</span>
<span
v-if="isPreparing"
class="text"
>
{{ $t('call.startNew') }}
</span>
<span
v-else-if="isInitiating"
class="text"
>
{{ $t('call.initiating') }}
</span>
<span
v-else-if="isRinging"
class="text"
>
{{ $t('call.ringing') }}
</span>
<span
v-else-if="isEnded"
class="text"
>
{{ $t('call.ended') }}
</span>
<span
v-else-if="isIncoming"
class="text"
>
{{ $t('call.incoming') }}
</span>
<span
v-else
class="text"
>
{{ $t('call.call') }}
</span>
<q-btn v-if="isFullscreenEnabled && !isMobile" round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="toggleFullscreen()" icon="fullscreen exit"/>
<q-btn v-else-if="!isMobile" round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="toggleFullscreen()" icon="fullscreen"/>
<q-btn round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="close()" icon="clear"/>
<q-btn
v-if="isFullscreenEnabled && !isMobile"
round
:small="!isFullscreenEnabled"
slot="right"
class="no-shadow"
@click="toggleFullscreen()"
icon="fullscreen exit"
/>
<q-btn
v-else-if="!isMobile"
round
:small="!isFullscreenEnabled"
slot="right"
class="no-shadow"
@click="toggleFullscreen()"
icon="fullscreen"
/>
<q-btn
round
:small="!isFullscreenEnabled"
slot="right"
class="no-shadow"
@click="close()"
icon="clear"
/>
</q-card-title>
<q-card-main>
<q-alert v-if="desktopSharingInstall" v-model="desktopSharingInstall"
color="warning" :actions="desktopSharingAlertActions">
<q-alert
v-if="desktopSharingInstall"
v-model="desktopSharingInstall"
color="warning"
:actions="desktopSharingAlertActions"
>
{{ $t('call.desktopSharingNotInstalled') }}
</q-alert>
<div class="csc-call-info">
<q-field v-show="isPreparing" :helper="$t('call.inputNumber')" :count="64" dark
:error="validationEnabled && phoneNumberError" :error-label="$t('call.inputValidNumber')">
<q-input type="text" :float-label="$t('call.number')" :value="phoneNumber" @input="inputPhoneNumber"
ref="numberInput" dark clearable :max="64" @blur="phoneNumberBlur" @focus="phoneNumberFocus"
@keypress.space.prevent @keydown.space.prevent />
</q-field>
<div v-if="!isPreparing" class="phone-number">
<q-icon v-if="isCalling && (localMediaType == 'audioVideo' || remoteMediaType == 'audioVideo')"
name="videocam" color="primary" size="26px"/>
<q-icon v-else-if="isCalling && (localMediaType == 'audioOnly' || remoteMediaType == 'audioVideo')"
name="mic" color="primary" size="26px"/>
<csc-phone-number-input
ref="phoneNumberInput"
v-if="isPreparing"
:enabled="isPhoneNumberInputEnabled"
/>
<div
v-if="!isPreparing"
class="phone-number"
>
<q-icon
v-if="isCalling && (localMediaType == 'audioVideo' || remoteMediaType == 'audioVideo')"
name="videocam"
color="primary"
size="26px"
/>
<q-icon
v-else-if="isCalling && (localMediaType == 'audioOnly' || remoteMediaType == 'audioVideo')"
name="mic"
color="primary"
size="26px"
/>
{{ getNumber | destinationFormat }}
</div>
<div v-if="isEnded" class="ended-reason">{{ getEndedReason }}</div>
<div
v-if="isEnded"
class="ended-reason"
>
{{ getEndedReason }}
</div>
<csc-call-dialpad
class="csc-call-dialpad"
v-if="isDialpadEnabled"
@inserted="dialpadInserted"
/>
</div>
<div class="csc-call-media">
<csc-media :class="mediaPreviewClasses" id="local-media" :muted="true"
v-show="isCalling" :stream="localMediaStream" />
<csc-media class="csc-media-remote" id="remote-media" :muted="isMuted"
v-show="isEstablished" :stream="remoteMediaStream" />
<csc-media
:class="mediaPreviewClasses"
id="local-media"
:muted="true"
v-show="isCalling"
:stream="localMediaStream"
/>
<csc-media
class="csc-media-remote"
id="remote-media"
:muted="isMuted"
v-show="isEstablished"
:stream="remoteMediaStream"
/>
</div>
</q-card-main>
<q-card-actions align="center">
<q-btn v-if="isEstablished" round :small="!isFullscreenEnabled" color="primary" @click="toggleAudio()" :icon="toggleAudioIcon" />
<q-btn v-if="isEstablished && localMediaType == 'audioVideo'" round :small="!isFullscreenEnabled" color="primary" @click="toggleVideo()" :icon="toggleVideoIcon" />
<q-btn v-if="isEstablished" round :small="!isFullscreenEnabled" color="primary" @click="toggleMute()" :icon="toggleMuteIcon" />
<q-btn v-if="isPreparing" round :small="!isFullscreenEnabled" color="primary" @click="call('audioOnly')" icon="mic" />
<q-btn v-if="isPreparing" round :small="!isFullscreenEnabled" color="primary" @click="call('audioVideo')" icon="videocam" />
<q-btn v-if="isPreparing && !isMobile" round :small="!isFullscreenEnabled" color="primary" @click="call('audioScreen')" icon="computer" />
<q-btn v-if="isCalling" round :small="!isFullscreenEnabled" color="negative" @click="hangUp()" icon="call end" />
<q-btn v-if="isEnded" round :small="!isFullscreenEnabled" color="negative" @click="init()" icon="clear"/>
<q-btn v-if="isIncoming" round :small="!isFullscreenEnabled" color="primary" @click="accept('audioOnly')" icon="mic" />
<q-btn v-if="isIncoming" round :small="!isFullscreenEnabled" color="primary" @click="accept('audioVideo')" icon="videocam" />
<q-btn v-if="isIncoming && !isMobile" round :small="!isFullscreenEnabled" color="primary" @click="accept('audioScreen')" icon="computer" />
<q-btn v-if="isIncoming" round :small="!isFullscreenEnabled" color="negative" @click="decline()" icon="call end" />
<q-btn
v-if="isEstablished"
round
:small="!isFullscreenEnabled"
color="primary"
@click="toggleAudio()"
:icon="toggleAudioIcon"
/>
<q-btn
v-if="isEstablished && localMediaType == 'audioVideo'"
round
:small="!isFullscreenEnabled"
color="primary"
@click="toggleVideo()"
:icon="toggleVideoIcon"
/>
<q-btn
v-if="isEstablished"
round
:small="!isFullscreenEnabled"
color="primary"
@click="toggleMute()"
:icon="toggleMuteIcon"
/>
<q-btn
v-if="isPreparing"
round
:small="!isFullscreenEnabled"
color="primary"
@click="call('audioOnly')"
icon="mic"
/>
<q-btn
v-if="isPreparing"
round
:small="!isFullscreenEnabled"
color="primary"
@click="call('audioVideo')"
icon="videocam"
/>
<q-btn
v-if="isPreparing && !isMobile"
round
:small="!isFullscreenEnabled"
color="primary"
@click="call('audioScreen')"
icon="computer"
/>
<q-btn
v-if="isCalling"
round
:small="!isFullscreenEnabled"
color="negative"
@click="hangUp()"
icon="call end"
/>
<q-btn
v-if="isEnded"
round
:small="!isFullscreenEnabled"
color="negative"
@click="init()"
icon="clear"
/>
<q-btn
v-if="isIncoming"
round
:small="!isFullscreenEnabled"
color="primary"
@click="accept('audioOnly')"
icon="mic"
/>
<q-btn
v-if="isIncoming"
round
:small="!isFullscreenEnabled"
color="primary"
@click="accept('audioVideo')"
icon="videocam"
/>
<q-btn
v-if="isIncoming && !isMobile"
round
:small="!isFullscreenEnabled"
color="primary"
@click="accept('audioScreen')"
icon="computer"
/>
<q-btn
v-if="isDialpadButtonEnabled"
round
:small="!isFullscreenEnabled"
color="primary"
@click="toggleDialpad()"
icon="dialpad"
/>
<q-btn
v-if="isIncoming"
round
:small="!isFullscreenEnabled"
color="negative"
@click="decline()"
icon="call end"
/>
</q-card-actions>
</q-card>
</div>
</template>
<script>
import Vue from 'vue';
import _ from 'lodash';
import platformMixin from '../mixins/platform'
import { mapGetters } from 'vuex'
import CscMedia from './CscMedia'
import { normalizeNumber, rawNumber } from '../filters/number-format'
import numberFormat from '../filters/number-format'
import { showCallNotification } from '../helpers/ui'
import { getChromeExtensionUrl } from '../helpers/cdk-lib'
import CscCallDialpad from './CscCallDialpad'
import CscPhoneNumberInput from './call/CscPhoneNumberInput'
import {
QLayout,
QCard,
@ -100,7 +304,6 @@
QIcon,
QSpinnerRings,
Dialog,
Platform,
QAlert
} from 'quasar-framework'
@ -111,9 +314,13 @@
return {
phoneNumber: '',
phoneNumberError: false,
validationEnabled: false
validationEnabled: false,
dialpadEnabled: false
}
},
mixins: [
platformMixin
],
components: {
QLayout,
QCard,
@ -127,19 +334,27 @@
QIcon,
QSpinnerRings,
CscMedia,
QAlert
QAlert,
CscCallDialpad,
CscPhoneNumberInput
},
mounted() {
this.dialpadEnabled = this.isMobile;
},
methods: {
inputPhoneNumber(value) {
this.phoneNumber = normalizeNumber(value, Platform.is.mobile);
dialpadInserted(value) {
if(this.isEstablished) {
this.$store.dispatch('call/sendDTMF', value);
}
else {
this.$refs.phoneNumberInput.concat(value);
}
},
focusNumberInput() {
Vue.nextTick(() => {
this.$refs.numberInput.focus();
});
this.$refs.phoneNumberInput.focusInput();
},
blurNumberInput() {
this.$refs.numberInput.blur();
this.$refs.phoneNumberInput.blurInput();
},
init() {
this.phoneNumber = '';
@ -154,17 +369,12 @@
this.validationEnabled = false;
},
call(localMedia) {
this.validationEnabled = true;
if(!_.isEmpty(this.phoneNumber)) {
this.phoneNumberError = false;
if(this.$refs.phoneNumberInput.hasPhoneNumber()) {
this.$store.dispatch('call/start', {
number: rawNumber(this.phoneNumber),
number: this.$refs.phoneNumberInput.getRawPhoneNumber(),
localMedia: localMedia
});
}
else {
this.phoneNumberError = true;
}
},
accept(localMedia) {
this.$store.dispatch('call/accept', localMedia);
@ -200,10 +410,10 @@
}
},
playIncomingSound() {
this.$refs.incomingRinging.play();
this.$refs.incomingSound.play();
},
stopIncomingSound() {
this.$refs.incomingRinging.stop();
this.$refs.incomingSound.pause();
},
toggleAudio() {
if(this.isAudioEnabled) {
@ -231,6 +441,9 @@
},
toggleFullscreen() {
this.$emit('fullscreen');
},
toggleDialpad() {
this.dialpadEnabled = !this.dialpadEnabled;
}
},
computed: {
@ -268,16 +481,6 @@
}
return classes;
},
// formattedPhoneNumber: {
// get() {
// return normalizeNumber(this.phoneNumber);
// },
// set(value) {
// this.validationEnabled = true;
// this.phoneNumberError = false;
// this.phoneNumber = rawNumber(value);
// }
// },
localMediaStream() {
if(this.$store.state.call.localMediaStream !== null) {
return this.$store.state.call.localMediaStream.getStream();
@ -317,9 +520,6 @@
'callState',
'desktopSharingInstall'
]),
isMobile() {
return Platform.is.mobile;
},
desktopSharingAlertActions() {
var self = this;
return [
@ -337,30 +537,45 @@
}
}
]
},
isPhoneNumberInputEnabled () {
return !(this.isDialpadEnabled && this.isMobile);
},
isDialpadEnabled() {
return this.dialpadEnabled && (this.isPreparing || this.isEstablished);
},
isDialpadButtonEnabled() {
return this.isPreparing || this.isEstablished;
},
callClasses() {
let callClasses = ['csc-call'];
if(this.isEstablished) {
callClasses.push('csc-call-established');
}
return callClasses;
}
},
watch: {
callState(state) {
if(state === 'incoming') {
showCallNotification(numberFormat(this.getNumber));
this.$refs.incomingSound.play();
}
else if (state === 'input') {
this.focusNumberInput();
this.$refs.incomingSound.pause();
this.playIncomingSound();
}
else if (state === 'ringing') {
this.$refs.incomingSound.play();
this.playIncomingSound();
}
else if (state === 'input') {
this.stopIncomingSound();
}
else {
this.$refs.incomingSound.pause();
this.stopIncomingSound();
}
}
}
}
</script>
<style lang="stylus">
<style lang="stylus" rel="stylesheet/stylus">
@import '../themes/quasar.variables.styl';
.csc-call {
@ -430,4 +645,9 @@
margin-bottom: 16px;
}
.csc-call.csc-call-established
.csc-call-dialpad
padding-top 0
padding-bottom 16px
</style>

@ -0,0 +1,114 @@
<template>
<div class="csc-dialpad row justify-center">
<div
class="column"
>
<div
class="csc-dialpad-btn-group"
v-for="(keyRow, rowIndex) in keys"
:key="rowIndex"
>
<div
class="csc-dialpad-btn"
v-for="(key, keyIndex) in keyRow"
:key="rowIndex + ':' + keyIndex"
>
<q-btn
color="primary"
round
outline
:small="!isFullscreenEnabled && !isMobile"
@click="insert(key)"
>
{{ key }}
</q-btn>
</div>
</div>
</div>
</div>
</template>
<script>
import platformMixin from '../mixins/platform'
import {
QBtn,
QIcon
} from 'quasar-framework'
import { mapGetters } from 'vuex'
export default {
name: 'csc-call-dialpad',
data () {
return {}
},
components: {
QBtn,
QIcon
},
mixins: [
platformMixin
],
computed: {
...mapGetters('layout', [
'isFullscreenEnabled'
]),
...mapGetters('call', [
'isEstablished'
]),
isDialpadSmall() {
let classes = ['row', 'justify-center'];
if(!this.isFullscreenEnabled) {
classes.push('small-dialpad');
}
else {
classes.push('csc-dialpad');
}
return classes;
},
keys() {
return [
['1','2','3'],
['4','5','6'],
['7','8','9'],
['*','0','#']
];
}
},
methods: {
insert(value) {
this.$emit('inserted', value);
if(this.isEstablished) {
this.$store.dispatch('call/sendDTMF', value);
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../themes/quasar.variables.styl'
.csc-dialpad
padding 16px
padding-bottom 0
.csc-dialpad-btn
display inline-block
margin-left 16px
.q-btn-inner
font-size 22px
.q-btn-small
.q-btn-inner
font-size 18px
.csc-dialpad-btn:first-child
margin-left 0
.csc-dialpad-btn-group
display: block
margin-bottom 8px
.csc-dialpad-btn-group:last-child
margin-bottom 0
</style>

@ -0,0 +1,115 @@
<template>
<q-field
dark
:count="maxLength"
:helper="helperMessage"
:error="$v.phoneNumber.$error"
:error-label="errorMessage"
>
<q-input
ref="inputField"
dark
clearable
type="text"
:float-label="$t('call.number')"
:value="phoneNumber"
:error="$v.phoneNumber.$error"
:max-length="maxLength"
:readonly="!enabled"
@input="inputPhoneNumber"
@keypress.space.prevent
@keydown.space.prevent
/>
</q-field>
</template>
<script>
import Vue from 'vue'
import _ from 'lodash'
import {
maxLength
} from 'vuelidate/lib/validators'
import {
normalizeNumber,
rawNumber
} from '../../filters/number-format'
import {
QField,
QInput
} from 'quasar-framework'
import platformMixin from '../../mixins/platform'
export default {
name: 'csc-phone-number-input',
components: {
QField,
QInput
},
data () {
return {
phoneNumber: ''
}
},
validations: {
phoneNumber: {
maxLength: maxLength(64)
}
},
props: {
maxLength: {
type: Number,
default: 64
},
enabled: {
type: Boolean,
default: true
}
},
mixins: [
platformMixin
],
computed: {
errorMessage() {
return this.$t('call.inputValidNumber');
},
helperMessage() {
return this.$t('call.inputNumber');
},
inputReadonly() {
return this.isMobile;
}
},
methods: {
inputPhoneNumber(value) {
this.phoneNumber = normalizeNumber(value, this.isMobile);
this.$v.phoneNumber.$touch();
},
getPhoneNumber() {
return this.phoneNumber;
},
getRawPhoneNumber() {
return rawNumber(this.getPhoneNumber());
},
hasPhoneNumber() {
return !_.isEmpty(this.phoneNumber);
},
concat(str) {
this.inputPhoneNumber(this.phoneNumber + "" + str);
},
focusInput() {
Vue.nextTick(() => {
this.$refs.inputField.focus();
});
},
blurInput() {
Vue.nextTick(() => {
this.$refs.inputField.blur();
});
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -1,101 +0,0 @@
<template>
<div class="csc-card-collapsible" :id="getId">
<q-card>
<q-item :link="collapsible" @click="toggleCollapse()" class="csc-card-title">
<q-item-side class="csc-card-left-icons">
<q-item-tile color="secondary" :icon="firstIcon" />
<q-item-tile color="secondary" :icon="secondIcon" />
</q-item-side>
<q-item-main>
<q-item-tile label>
<slot name="title" />
</q-item-tile>
<q-item-tile sublabel>
{{ sublabel }}
</q-item-tile>
</q-item-main>
<q-item-side v-if="collapsible" right>
<q-item-tile color="secondary" icon="keyboard_arrow_down" />
</q-item-side>
</q-item>
<q-card-main v-show="isExpanded">
<slot name="main"></slot>
</q-card-main>
<slot name="footer"></slot>
</q-card>
</div>
</template>
<script>
import { QCard, QCardTitle, QCardMain, QIcon, QItem,
QItemSide, QItemMain, QItemTile } from 'quasar-framework'
export default {
name: 'csc-card-collapsible',
props: [
'listItem',
'collapsible',
'firstIcon',
'secondIcon',
'sublabel',
'id'
],
data() {
return {
expanded: false
}
},
components: {
QCard,
QCardTitle,
QCardMain,
QIcon,
QItem,
QItemSide,
QItemMain,
QItemTile
},
computed: {
isExpanded() {
return this.expanded;
},
getId() {
return this.id;
}
},
methods: {
toggleCollapse() {
if (this.collapsible) {
this.expanded = !this.expanded;
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../../themes/quasar.variables.styl'
.csc-card-collapsible
ul
list-style none
font-size 1rem
line-height 2rem
strong
font-weight 500
.q-item-icon
font-size 22px
padding-right 12px
line-height 2rem
div.q-item-label > span
font-size 18px
color #0c0c0c
font-weight 400
div.q-item-sublabel
margin-top 5px
.csc-card-left-icons
margin-left 15px
.csc-card-title
padding 16px
</style>

@ -1,26 +0,0 @@
<template>
<q-chip pointing="left" color="primary" class="csc-number-chip">
<span class="number-pilot">{{ item.primary_number | number(item.pbx_extension) }}</span>
<span class="number-ext">{{ item.pbx_extension }}</span>
</q-chip>
</template>
<script>
import { QChip } from 'quasar-framework'
export default {
name: 'csc-number-chip',
props: ['item'],
data () {
return {}
},
components: {
QChip
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.csc-number-chip .number-ext {
font-weight: bold;
}
</style>

@ -266,7 +266,7 @@
"call": "Established",
"inputNumber": "Input a phone number",
"inputValidNumber": "Input a valid phone number",
"number": "Number",
"number": "Phone number",
"endCall": "End Call",
"endCallDialog": "You are about to end the current call. Are you sure?",
"notificationTitle": "Incoming call from {number}",

@ -0,0 +1,13 @@
import {
Platform
} from 'quasar-framework'
export default {
computed: {
isMobile() {
return Platform.is.mobile;
}
}
}

@ -306,6 +306,15 @@ export class RtcEngineCall {
}
}
sendDTMF(char) {
if(this.localCall !== null) {
this.localCall.sendDTMF(char);
}
else if (this.remoteCall !== null) {
this.remoteCall.sendDTMF(char);
}
}
getCall() {
if(this.localCall !== null) {
return this.localCall;

@ -34,7 +34,8 @@ export default {
muted: false,
caller: false,
callee: false,
desktopSharingInstall: false
desktopSharingInstall: false,
dtmf: null
},
getters: {
getNumber(state) {
@ -136,6 +137,9 @@ export default {
},
desktopSharingInstall(state) {
return state.desktopSharingInstall;
},
dtmfState(state) {
return state.dtmf;
}
},
mutations: {
@ -227,6 +231,9 @@ export default {
},
desktopSharingInstall(state) {
state.desktopSharingInstall = true;
},
sendDTMF(state, value) {
state.dtmf = value;
}
},
actions: {
@ -323,6 +330,12 @@ export default {
},
hideCall(context) {
context.commit('layout/hideRight', null, { root: true });
},
sendDTMF(context, value) {
context.commit('sendDTMF', value);
if(Vue.call.hasRunningCall()) {
Vue.call.sendDTMF(value);
}
}
}
};

Loading…
Cancel
Save