TT#23603 Call: As a Customer I want to input a number and start a call

Change-Id: Iaf1aa61fb0d5f7e0fe6ef4c427911a50933a09b8
changes/25/16525/5
Hans-Peter Herzog 8 years ago
parent a5d6d1bfb8
commit 6ab38fcbb7

@ -94,6 +94,35 @@ Now you can log in to csc with one of the normal subscriber you just created. UR
```https://<your-ip-address>/csc```
### How to add new npm package
1. Ensure that you have a clean node_modules folder
```
rm -R node_modules/
npm install
```
1. Remove obsolete shrinkwrap file
```rm npm-shrinkwrap.json```
1. Install new package(s)
```npm install packageA packageB --save-dev```
1. Generate new shrinkwrap file including all dependencies
```npm shrinkwrap --dev```
You should see the following result in console:
```wrote npm-shrinkwrap.json```
1. Add new shrinkwrap file to git
```git add .```
## Contributing
See our [Contributing Guide](./CONTRIBUTING.md) file) for information on how to contribute.

@ -39,6 +39,7 @@ webpackConfig.watchOptions = {
'debian',
'dist',
'node_modules',
't',
'templates'
]
}

@ -35,16 +35,6 @@ module.exports = {
},
module: {
rules: [
// { // eslint
// enforce: 'pre',
// test: /\.(vue|js)$/,
// loader: 'eslint-loader',
// include: projectRoot,
// exclude: /node_modules/,
// options: {
// formatter: require('eslint-friendly-formatter')
// }
// },
{
test: /\.js$/,
loader: 'babel-loader',

19468
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{
"name": "ngcp-csc-ui",
"productName": "Customer Self-Care Web UI",
"version": "0.1.1",
"version": "0.2.1",
"description": "Customer Self-Care Web UI",
"author": "Hans-Peter Herzog <hherzog@sipwise.com>",
"scripts": {
@ -9,25 +9,18 @@
"dev": "node build/script.dev.js",
"build": "node build/script.build.js mat",
"dev-build": "CSC_SOURCE_MAP=1 CSC_WATCH=1 node build/script.build.js mat",
"lint": "eslint --ext .js,.vue src",
"test": "karma start ./karma.js --single-run"
},
"dependencies": {
"ansi-html": "0.0.7",
"ansi-regex": "^3.0.0",
"babel-runtime": "^6.25.0",
"core-js": "^2.5.1",
"html-entities": "^1.2.1",
"lodash": "^4.17.4",
"quasar-extras": "0.x",
"quasar-framework": "0.14.4",
"strip-ansi": "^4.0.0",
"vue": "2.3.4",
"vue-i18n": "^7.3.0",
"vue-resource": "^1.3.4",
"vue-router": "^2.7.0",
"vuex": "^2.4.1",
"vuex-router-sync": "^4.3.2"
"quasar-framework": "^0.14.4",
"vue": "^2.5.0",
"vue-i18n": "7.3.2",
"vue-resource": "1.3.4",
"vue-router": "^2.5.0",
"vuex": "2.5.0",
"vuex-router-sync": "4.3.2"
},
"devDependencies": {
"autoprefixer": "^6.4.0",
@ -57,6 +50,7 @@
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"glob": "^7.1.2",
"google-libphonenumber": "3.0.7",
"html-webpack-plugin": "^2.30.1",
"http-proxy-middleware": "^0.17.0",
"json-loader": "^0.5.7",
@ -67,6 +61,7 @@
"karma-mocha": "^1.3.0",
"karma-webpack": "^2.0.4",
"load-script": "1.0.0",
"lodash": "4.17.4",
"mocha": "^4.0.0",
"opn": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
@ -79,7 +74,7 @@
"url-loader": "^0.5.7",
"vue-loader": "^13.0.5",
"vue-style-loader": "^3.0.3",
"vue-template-compiler": "~2.3.4",
"vue-template-compiler": "^2.5.0",
"webpack": "^3.6.0",
"webpack-dev-middleware": "^1.12.0",
"webpack-hot-middleware": "^2.19.1",

@ -0,0 +1,211 @@
<template>
<div class="csc-call">
<q-card flat color="secondary">
<q-card-title>
<q-icon v-if="isCalling && getMediaType == 'audioOnly'" name="mic" color="primary" size="26px"/>
<q-icon v-else-if="isCalling && getMediaType == 'audioVideo'" name="videocam" color="primary" size="26px"/>
<q-icon v-else-if="isEnded" name="error" color="primary" size="26px"/>
<q-icon v-else name="call made" 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 class="text">{{ $t('call.call') }}</span>
<q-btn round small slot="right" class="no-shadow" @click="close()" icon="clear"/>
</q-card-title>
<q-card-main>
<div v-if="isTrying" class="csc-spinner"><q-spinner-rings color="primary" :size="60" /></div>
<div v-if="isCalling" class="phone-number">{{ getNumber }}</div>
<csc-media v-if="isCalling && localMediaStream != null" :stream="localMediaStream" />
<q-field v-if="isPreparing" :helper="$t('call.inputNumber')" :error="validationEnabled && phoneNumberError"
:error-label="$t('call.inputValidNumber')" :count="64" dark>
<q-input :float-label="$t('call.number')" v-model="formattedPhoneNumber" dark clearable max="64"
@blur="phoneNumberBlur()" @focus="phoneNumberFocus()"/>
</q-field>
<div v-if="isEnded" class="ended-reason">
{{ getEndedReason }}
</div>
</q-card-main>
<q-card-actions align="center">
<q-btn v-if="isPreparing" round small color="primary" @click="call('audioOnly')" icon="mic" />
<q-btn v-if="isPreparing" round small color="primary" @click="call('audioVideo')" icon="videocam" />
<q-btn v-if="isCalling" round small color="negative" @click="hangUp()" icon="call end" />
<q-btn v-if="isEnded" round small color="negative" @click="init()" icon="clear"/>
</q-card-actions>
</q-card>
</div>
</template>
<script>
import _ from 'lodash';
import { mapState, mapGetters } from 'vuex'
import CscMedia from './CscMedia'
import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput,
QCardActions, QBtn, QIcon, Loading, Alert, QSpinnerRings } from 'quasar-framework'
import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'
var phoneUtil = PhoneNumberUtil.getInstance();
export default {
name: 'csc-call',
props: ['region'],
data () {
return {
phoneNumber: '',
parsedPhoneNumber: null,
phoneNumberError: false,
validationEnabled: false
}
},
components: {
QLayout,
QCard,
QCardTitle,
QCardSeparator,
QCardMain,
QField,
QInput,
QCardActions,
QBtn,
QIcon,
QSpinnerRings,
CscMedia
},
methods: {
init() {
this.phoneNumber = '';
this.parsedPhoneNumber = null;
this.validationEnabled = false;
this.phoneNumberError = false;
this.$store.commit('call/inputNumber');
},
phoneNumberFocus() {
this.validationEnabled = true;
},
phoneNumberBlur() {
this.validationEnabled = false;
},
call(localMedia) {
this.validationEnabled = true;
if(this.parsedPhoneNumber !== null) {
this.phoneNumberError = false;
this.$store.dispatch('call/start', {
number: this.phoneNumber,
localMedia: localMedia
});
} else {
this.phoneNumberError = true;
}
},
hangUp() {
this.$store.dispatch('call/hangUp');
},
close() {
this.$store.commit('call/inputNumber');
this.$emit('close');
}
},
computed: {
formattedPhoneNumber: {
get() {
if(this.parsedPhoneNumber !== null) {
return _.trim(phoneUtil.format(this.parsedPhoneNumber, PhoneNumberFormat.INTERNATIONAL));
} else {
return _.trim(this.phoneNumber);
}
},
set(value) {
this.validationEnabled = true;
this.phoneNumber = _.trim(value);
if(this.phoneNumber.match('^[1-9]')) {
this.phoneNumber = '+' + this.phoneNumber;
} else if(this.phoneNumber === '+') {
this.phoneNumber = '';
}
if(phoneUtil.isPossibleNumberString(this.phoneNumber, this.region)) {
try {
this.parsedPhoneNumber = phoneUtil.parse(this.phoneNumber, this.region);
this.phoneNumber = phoneUtil.format(this.parsedPhoneNumber, PhoneNumberFormat.E164);
this.phoneNumberError = false;
} catch(err) {
this.parsedPhoneNumber = null;
this.phoneNumberError = true;
}
} else {
this.parsedPhoneNumber = null;
this.phoneNumberError = true;
}
}
},
localMediaStream() {
if(this.$store.state.call.localMediaStream != null) {
return this.$store.state.call.localMediaStream.getStream();
} else {
return null;
}
},
remoteMediaStream() {
console.log(this.$refs.remoteMedia);
},
...mapGetters('call', [
'isPreparing',
'isInitiating',
'isTrying',
'isRinging',
'isCalling',
'isEnded',
'getNumber',
'getMediaType',
'getLocalMediaType',
'getEndedReason'
]),
hasLocalVideo() {
return this.getLocalMediaType !== null && this.getLocalMediaType.match(/(v|V)ideo/) !== null;
}
}
}
</script>
<style lang="stylus">
.csc-call .q-card {
margin:0;
}
.csc-call .q-card-main {
padding: 0;
}
.csc-call .q-field {
margin: 0px;
padding-left: 16px;
padding-right: 16px;
}
.csc-call .q-card-actions {
padding: 16px;
}
.csc-spinner {
text-align: center;
margin-bottom: 16px;
}
.q-card-title .text {
color: #adb3b8;
}
.csc-call .phone-number {
font-size: 18px;
text-align: center;
color: #adb3b8;
margin-bottom: 16px;
}
.csc-call .ended-reason {
font-size: 18px;
text-align: center;
color: #adb3b8;
}
</style>

@ -0,0 +1,42 @@
<template>
<div class="csc-media">
<video ref="media"></video>
</div>
</template>
<script>
import _ from 'lodash';
export default {
name: 'csc-media',
props: ['stream'],
data () {
return {
currentStream: this.stream
}
},
mounted() {
if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.srcObject)) {
this.$refs.media.srcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.mozSrcObject)) {
this.$refs.media.mozSrcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
_.isObject(URL) && _.isFunction(URL.createObjectURL)) {
this.$refs.media.src = URL.createObjectURL(this.currentStream);
}
},
components: {},
methods: {},
computed: {}
}
</script>
<style lang="stylus">
.csc-media {
width: 100%
}
.csc-media video {
width: 100%
}
</style>

@ -10,9 +10,9 @@
</template>
<script>
import { QIcon, QFixedPosition } from 'quasar-framework'
import { QIcon, QFixedPosition, QFab, QFabAction, QTooltip } from 'quasar-framework'
export default {
name: 'page',
name: 'csc-page',
props: [
'title'
],
@ -21,7 +21,10 @@
},
components: {
QIcon,
QFixedPosition
QFixedPosition,
QFab,
QFabAction,
QTooltip
}
}
</script>
@ -30,6 +33,7 @@
@import '../../src/themes/app.variables.styl';
@import '../../src/themes/quasar.variables.styl';
.page {
position: relative;
padding: 60px;
padding-top: 100px;
}
@ -42,7 +46,6 @@
}
.page .page-title {
right: 0;
padding: 30px;
padding-left: 60px;
padding-right: 60px;
@ -50,6 +53,11 @@
z-index: 1000;
}
.page .page-button {
padding-top: 20px;
padding-right: 60px;
}
@media (max-width: $breakpoint-sm) {
.page {
padding: 30px;
@ -62,7 +70,6 @@
}
.page .page-title {
right: 0;
padding: 30px;
background-color: white;
z-index: 1000;

@ -1,5 +1,5 @@
<template>
<q-layout ref="layout" view="lHr LpR lFr" :right-breakpoint="1100">
<q-layout ref="layout" view="lHh LpR lFf" :right-breakpoint="1100">
<q-toolbar slot="header">
<q-btn flat @click="$refs.layout.toggleLeft()">
<q-icon name="menu"/>
@ -82,8 +82,9 @@
</q-side-link>
</q-collapsible>
</q-list>
<div id="page-action-button">
<q-fab v-if="hasCommunicationCapabilities"
<router-view />
<q-fixed-position corner="top-right" :offset="[20, 20]" class="page-button transition-generic">
<q-fab v-if=""
color="primary" icon="question answer"
active-icon="clear" direction="down" flat>
<q-fab-action v-if="hasFaxCapability" color="primary" @click="" icon="fa-fax">
@ -92,12 +93,12 @@
<q-fab-action v-if="hasSmsCapability" color="primary" @click="" icon="fa-send">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendSms') }}</q-tooltip>
</q-fab-action>
<q-fab-action v-if="isCallAvailable" color="primary" @click="" icon="fa-phone">
<q-fab-action v-if="isCallAvailable" color="primary" @click="call()" icon="fa-phone">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('startCall') }}</q-tooltip>
</q-fab-action>
</q-fab>
</div>
<router-view />
</q-fixed-position>
<csc-call ref="cscCall" slot="right" @close="$refs.layout.hideRight()" region="DE" />
</q-layout>
</template>
@ -105,6 +106,7 @@
import _ from 'lodash';
import { startLoading, stopLoading, showGlobalError, showToast } from '../../helpers/ui'
import { mapState, mapGetters } from 'vuex'
import CscCall from '../CscCall'
import {
QLayout,
QToolbar,
@ -123,12 +125,13 @@
QTooltip,
QSideLink,
QTransition,
QCollapsible
} from 'quasar'
QCollapsible,
} from 'quasar-framework'
export default {
name: 'default',
mounted: function() {
this.$refs.layout.showLeft();
this.$refs.layout.hideRight();
if(!this.hasUser) {
startLoading();
this.$store.dispatch('user/initUser').then(()=>{
@ -159,7 +162,8 @@
QTooltip,
QSideLink,
QTransition,
QCollapsible
QCollapsible,
CscCall
},
computed: {
...mapGetters('call', [
@ -185,12 +189,17 @@
methods: {
showInitialToasts() {
if(this.isCallAvailable) {
showToast(this.$i18n.t('toasts.callIsAvailable'));
showToast(this.$i18n.t('toasts.callAvailable'));
}
if(this.hasCallInitFailure) {
showToast(this.$i18n.t('toasts.callIsNotAvailable'));
showToast(this.$i18n.t('toasts.callNotAvailable'));
}
},
call() {
this.$refs.layout.showRight();
this.$refs.cscCall.init();
},
logout() {
startLoading();
this.$store.dispatch('user/logout').then(()=>{
@ -206,19 +215,6 @@
@import '../../../src/themes/app.variables.styl';
@import '../../../src/themes/quasar.variables.styl';
#page-action-button {
z-index: 1001;
position: fixed;
top: ($toolbar-min-height + 15)px;
right: 55px;
}
@media (max-width: $breakpoint-sm) {
#page-action-button {
right: 25px;
}
}
#main-menu {
padding-top:60px;
}

@ -1,11 +1,11 @@
<template>
<page :title="$t('pages.callBlocking' + suffix + '.title')">
<csc-page :title="$t('pages.callBlocking' + suffix + '.title')">
<q-field id="toggle-call-blocking">
<csc-toggle :label="toggleButtonLabel" @change="toggle" :enabled="enabled"/>
</q-field>
<div id="add-number-form">
<q-field v-if="!addFormEnabled">
<q-btn color="primary"
<q-btn flat color="primary"
icon="fa-plus"
@click="enableAddForm()">{{ $t('pages.callBlocking' + suffix + '.addNumberButton') }}</q-btn>
</q-field>
@ -13,37 +13,39 @@
<q-field :error="addFormError" :error-label="$t('pages.callBlocking' + suffix + '.addInputError')">
<q-input type="text" float-label="Number" v-model="newNumber" clearable @keyup.enter="addNumber()" />
</q-field>
<q-btn @click="disableAddForm()">{{ $t('buttons.cancel') }}</q-btn>
<q-btn color="primary" icon-right="fa-save" @click="addNumber()">{{ $t('buttons.save') }}</q-btn>
<q-btn flat @click="disableAddForm()">{{ $t('buttons.cancel') }}</q-btn>
<q-btn flat color="primary" icon-right="fa-save" @click="addNumber()">{{ $t('buttons.save') }}</q-btn>
</div>
</div>
<div>
<q-card class="blocked-number" v-for="(number, index) in numbers">
<q-card-title>
<q-icon v-if="!(editing && editingIndex == index)" name="fa-ban" color="secondary" size="22px"/>
<span v-if="!(editing && editingIndex == index)" @click="editNumber(index)">{{ number }}</span>
<q-input autofocus v-if="editing && editingIndex == index" type="text" float-label="Number"
v-model="editingNumber" @keyup.enter="saveNumber(index)" />
<q-icon v-if="editing && editingIndex == index" slot="right"
name="fa-save" @click="saveNumber(index)" class="cursor-pointer"></q-icon>
<q-icon v-if="!(editing && editingIndex == index)" slot="right"
name="fa-edit" @click="editNumber(index)" class="cursor-pointer"></q-icon>
<q-icon v-if="!(editing && editingIndex == index)" slot="right"
name="fa-remove" @click="removeNumber(index)" class="cursor-pointer"></q-icon>
<q-btn color="primary" flat small round v-if="editing && editingIndex == index" slot="right"
icon="fa-save" @click="saveNumber(index)" class="cursor-pointer">{{ $t('buttons.save') }}</q-btn>
<q-btn color="primary" flat small round v-if="editing && editingIndex == index" slot="right"
icon="clear" @click="saveNumber(index)" class="cursor-pointer">{{ $t('buttons.cancel') }}</q-btn>
<q-btn color="primary" flat small round v-if="!(editing && editingIndex == index)" slot="right"
icon="fa-edit" @click="editNumber(index)" class="cursor-pointer">{{ $t('buttons.edit') }}</q-btn>
<q-btn color="primary" flat small round v-if="!(editing && editingIndex == index)" slot="right"
icon="delete" @click="removeNumber(index)" class="cursor-pointer">{{ $t('buttons.remove') }}</q-btn>
</q-card-title>
</q-card>
<q-inner-loading :visible="listLoading">
<q-spinner-mat size="50px" color="primary"></q-spinner-mat>
</q-inner-loading>
</div>
</page>
</csc-page>
</template>
<script>
import _ from 'lodash';
import { startLoading, stopLoading, showGlobalError, showToast } from '../../../helpers/ui'
import Page from '../../Page'
import CscPage from '../../CscPage'
import CscToggle from '../../form/CscToggle'
import { QInput, QCard, QBtn, QField, QIcon, QCardTitle, Dialog, QSpinnerMat, QToggle,
import { QInput, QCard, QBtn, QField, QIcon, QCardTitle, QCardActions, Dialog, QSpinnerMat, QToggle,
Toast, QList, QItem, QItemMain, QCardMain, QInnerLoading } from 'quasar-framework'
export default {
name: 'csc-call-blocking',
@ -76,7 +78,6 @@
});
},
components: {
Page,
QToggle,
Toast,
QField,
@ -89,10 +90,12 @@
QCardMain,
QIcon,
QCardTitle,
QCardActions,
Dialog,
QInnerLoading,
QSpinnerMat,
CscToggle
CscToggle,
CscPage
},
computed: {
numbers (){
@ -210,9 +213,6 @@
#add-number-form {
margin-bottom:15px;
}
.blocked-number .q-card-title-extra .q-icon {
margin-left: 10px;
}
.blocked-number .q-input {
margin:0;
}

@ -1,13 +1,13 @@
<template>
<page title="Privacy">
<csc-page title="Privacy">
<q-toggle :label="(!callBlockingEnabled ? 'Hide' : 'Show') + ' Own Number'"
@input="toggle()" v-model="callBlockingEnabled"/>
</page>
</csc-page>
</template>
<script>
import { showToast } from '../../../helpers/ui'
import Page from '../../Page'
import CscPage from '../../CscPage'
import { QField, QToggle, Toast } from 'quasar-framework'
export default {
data () {
@ -23,7 +23,7 @@
});
},
components: {
Page,
CscPage,
QToggle,
Toast,
QField

@ -1,15 +1,15 @@
<template>
<page title="After Hours"></page>
<csc-page title="After Hours"></csc-page>
</template>
<script>
import Page from '../../Page'
import CscPage from '../../CscPage'
export default {
data () {
return {}
},
components: {
Page
CscPage
}
}
</script>

@ -1,15 +1,15 @@
<template>
<page title="Always"></page>
<csc-page title="Always"></csc-page>
</template>
<script>
import Page from '../../Page'
import CscPage from '../../CscPage'
export default {
data () {
return {}
},
components: {
Page
CscPage
}
}
</script>

@ -1,15 +1,15 @@
<template>
<page title="Company Hours"></page>
<csc-page title="Company Hours"></csc-page>
</template>
<script>
import Page from '../../Page'
import CscPage from '../../CscPage'
export default {
data () {
return {}
},
components: {
Page
CscPage
}
}
</script>

@ -1,15 +1,15 @@
<template>
<page title="Conversations"></page>
<csc-page title="Conversations"></csc-page>
</template>
<script>
import Page from '../Page'
import CscPage from '../CscPage'
export default {
data () {
return {}
},
components: {
Page
CscPage
}
}
</script>

@ -1,15 +1,15 @@
<template>
<page title="Devices"></page>
<csc-page title="Devices"></csc-page>
</template>
<script>
import Page from '../../Page'
import CscPage from '../../CscPage'
export default {
data () {
return {}
},
components: {
Page
CscPage
}
}
</script>

@ -1,17 +1,17 @@
<template>
<page title="PBX Groups"></page>
<csc-page title="PBX Groups"></csc-page>
</template>
<script>
import Page from '../../Page'
import CscPage from '../../CscPage'
import { QChip, QCard, QCardSeparator, QCardTitle, QCardMain,
QIcon, QPopover, QList, QItem, QItemMain } from 'quasar-framework'
import { mapState } from 'vuex'
export default {
components: {
Page,
CscPage,
QChip,
QCard,
QCardSeparator,

@ -1,15 +1,15 @@
<template>
<page title="Seats"></page>
<csc-page title="Seats"></csc-page>
</template>
<script>
import Page from '../../Page'
import CscPage from '../../CscPage'
export default {
data () {
return {}
},
components: {
Page
CscPage
}
}
</script>

@ -1,19 +1,19 @@
<template>
<page :title="$t('pages.reminder.title')">
<q-field class="reminder-field">
<q-toggle :label="$t('pages.reminder.title') + (active ? ' enabled':' disabled')" @input="toggleReminder()" v-model="active" />
</q-field>
<q-field class="reminder-field">
<q-datetime type="time" :disable="!active" no-clear=true v-model="timeConverted" :placeholder="$t('reminder.timeLabel')" @change="changeTime()" />
</q-field>
<q-field class="reminder-field">
<q-option-group :disable="!active" color="positive" type="radio" v-model="recurrence" @change="changeRecurrence()" :options="[
{ label: $t('pages.reminder.recurrence.once'), value: 'never' },
{ label: $t('pages.reminder.recurrence.weekdays'), value: 'weekdays' },
{ label: $t('pages.reminder.recurrence.always'), value: 'always' }
]" />
</q-field>
</page>
<csc-page :title="$t('pages.reminder.title')">
<q-field class="reminder-field">
<q-toggle :label="$t('pages.reminder.title') + (active ? ' enabled':' disabled')" @input="toggleReminder()" v-model="active" />
</q-field>
<q-field class="reminder-field">
<q-datetime type="time" :disable="!active" no-clear=true v-model="timeConverted" :placeholder="$t('reminder.timeLabel')" @change="changeTime()" />
</q-field>
<q-field class="reminder-field">
<q-option-group :disable="!active" color="positive" type="radio" v-model="recurrence" @change="changeRecurrence()" :options="[
{ label: $t('pages.reminder.recurrence.once'), value: 'never' },
{ label: $t('pages.reminder.recurrence.weekdays'), value: 'weekdays' },
{ label: $t('pages.reminder.recurrence.always'), value: 'always' }
]" />
</q-field>
</csc-page>
</template>
<script>
@ -22,7 +22,7 @@ import {
stopLoading,
showGlobalError
} from '../../helpers/ui'
import Page from '../Page'
import CscPage from '../CscPage'
import {
QField,
QToggle,
@ -49,7 +49,7 @@ export default {
});
},
components: {
Page,
CscPage,
QToggle,
Toast,
QDatetime,

@ -8,7 +8,12 @@
"buttons": {
"cancel": "Cancel",
"save": "Save",
"remove": "Remove"
"remove": "Remove",
"edit": "Edit"
},
"toasts": {
"callAvailable": "You are now able to start and receive calls",
"callNotAvailable": "Could not initialize call functionality properly"
},
"navigation": {
"conversations": {
@ -88,8 +93,14 @@
"recurrenceUpdatedMsg": "Recurrence updated!"
}
},
"toasts": {
"callIsAvailable:": "You are now able to start and receive calls",
"callIsNotAvailable": "Could not initialize call functionality properly"
"call": {
"startNew": "Start new call",
"initiating": "Call Initiating ...",
"ringing": "Call Ringing ...",
"ended": "Call ended",
"call": "Call",
"inputNumber": "Input a phone number",
"inputValidNumber": "Input a valid phone number",
"number": "Number"
}
}

@ -18,13 +18,14 @@ import { store } from './store'
import { i18n, locales } from './i18n'
import router from './router'
import { sync } from 'vuex-router-sync'
import filters from './filters'
import { RtcEngineCall } from './plugins/call'
Vue.use(VueResource);
Vue.config.productionTip = false;
Vue.use(Quasar); // Install Quasar Framework
Vue.use(RtcEngineCall);
if (__THEME === 'mat') {
require('quasar-extras/roboto-font')

@ -0,0 +1,154 @@
import _ from 'lodash';
import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib';
import { createSessionToken } from '../api/rtcsession';
export const LocalMedia = {
audioOnly: 'audioOnly',
audioVideo: 'audioVideo',
videoOnly: 'videoOnly',
audioScreen: 'audioScreen',
screenOnly: 'screenOnly'
};
export class NetworkNotConnected extends Error {
constructor(network) {
super();
this.name = this.constructor.name;
this.message = 'Network ' + network + ' is not connected';
this.network = network;
}
}
var rtcEngineCallInstance = null;
export class RtcEngineCall {
constructor(options) {
this.networkTag = 'sip';
this.client = null;
this.network = null;
this.currentCall = null;
this.loadedLibrary = null;
this.sessionToken = null;
}
initialize() {
return new Promise((resolve, reject)=>{
Promise.resolve().then(($loadedLibrary)=>{
this.loadedLibrary = $loadedLibrary;
return this.loadLibrary();
}).then(()=>{
return this.createSession();
}).then(($sessionToken)=>{
this.sessionToken = $sessionToken;
return this.connectNetwork($sessionToken);
}).then(($network)=>{
this.network = $network;
resolve();
}).catch((err)=>{
reject(err);
});
});
}
isAvailable() {
return this.network !== null && this.network.isConnected;
}
hasRunningCall() {
return this.currentCall !== null;
}
loadLibrary() {
return loadCdkLib();
}
createSession() {
return createSessionToken();
}
connectNetwork(session) {
return connectDefaultCdkNetwork(session);
}
createLocalMedia(localMedia) {
return new Promise((resolve, reject)=>{
var localMediaStream = new cdk.LocalMediaStream();
var hasAudio = localMedia === LocalMedia.audioOnly ||
localMedia === LocalMedia.audioVideo ||
localMedia === LocalMedia.audioScreen;
var hasVideo = localMedia === LocalMedia.audioVideo ||
localMedia === LocalMedia.videoOnly;
var hasScreen = localMedia === LocalMedia.audioScreen ||
localMedia === LocalMedia.screenOnly;
localMediaStream.queryMediaSources((sources) => {
if (hasAudio && _.isObject(sources.defaultAudio)) {
localMediaStream.setAudio(sources.defaultAudio);
}
if (hasVideo && _.isObject(sources.defaultVideo)) {
localMediaStream.setVideo(sources.defaultVideo);
} else if (hasScreen && _.isObject(sources.desktopSharing)) {
localMediaStream.setVideo(sources.desktopSharing);
}
});
localMediaStream.build((err)=>{
if(_.isObject(err)) {
reject(err);
} else {
resolve(localMediaStream);
}
});
});
}
start(peer, localMediaStream) {
peer = peer.replace('+', '');
if(this.network !== null) {
this.currentCall = this.network.call(peer, { localMediaStream: localMediaStream });
return this.currentCall;
} else {
throw new NetworkNotConnected(this.networkTag);
}
}
onIncoming(listener) {
if(this.network !== null) {
this.network.onIncomingCall((call)=>{
if(this.currentCall === null) {
this.currentCall = call;
listener(call);
}
});
} else {
throw new NetworkNotConnected(this.networkTag);
}
}
accept(localMediaStream) {
if(this.currentCall !== null) {
this.currentCall.accept({
localMediaStream: localMediaStream
});
}
}
hangUp() {
if(this.currentCall !== null) {
this.currentCall.end();
}
}
static getInstance() {
if(rtcEngineCallInstance === null) {
rtcEngineCallInstance = new RtcEngineCall();
}
return rtcEngineCallInstance;
}
static install(Vue, options) {
Vue.call = RtcEngineCall.getInstance();
}
}

@ -1,98 +1,213 @@
'use strict';
import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib';
import { createSessionToken } from '../api/rtcsession';
// import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib';
// import { createSessionToken } from '../api/rtcsession';
var cdkNetwork = null;
import Vue from 'vue';
export var CallState = {
input: 'input',
initiating: 'initiating',
ringing: 'ringing',
incoming: 'incoming',
established: 'established',
ended: 'ended'
};
export var MediaType = {
audio: 'audio',
audioVideo: 'audioVideo',
audioScreen: 'audioScreen'
};
export default {
namespaced: true,
state: {
loaded: false,
initFailure: false,
connected: false,
disconnectReason: '',
incoming: false,
incomingNumber: '',
outgoing: false,
outgoingNumber: ''
initialized: false,
initError: null,
endedReason: null,
callState: CallState.input,
number: null,
mediaType: null,
localMediaType: null,
localMediaStream: null,
remoteMediaStream: null
},
getters: {
getNumber(state, getters) {
return state.number;
},
getMediaType(state, getters) {
return state.mediaType;
},
getLocalMediaType(state, getters) {
return state.localMediaType
},
getEndedReason(state, getters) {
return state.endedReason;
},
isNetworkConnected(state, getters) {
return state.initialized;
},
isCallAvailable(state, getters) {
return state.loaded && state.connected;
return getters.isNetworkConnected;
},
hasCallInitFailure(state, getters) {
return state.initFailure;
return state.initError !== null;
},
isPreparing(state, getters) {
return state.callState === CallState.input;
},
isInitiating(state, getters) {
return state.callState === CallState.initiating;
},
isTrying(state, getters) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing;
},
isRinging(state, getters) {
return state.callState === CallState.ringing;
},
isCalling(state, getters) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing ||
state.callState === CallState.established;
},
isEnded(state, getters) {
return state.callState === CallState.ended;
}
},
mutations: {
load(state) {
state.loaded = true;
initSucceeded(state) {
state.initialized = true;
state.initError = null;
},
initFailure(state) {
state.initFailure = true;
initFailed(state, err) {
state.initialized = false;
state.initError = err;
},
connect(state) {
state.connected = true;
inputNumber(state) {
state.callState = CallState.input;
},
disconnect(state, reason) {
state.connected = false;
state.disconnectReason = reason;
startCalling(state, options) {
state.number = options.number;
state.mediaType = options.mediaType;
state.localMediaType = state.mediaType;
state.localMediaStream = options.localMediaStream;
state.callState = CallState.initiating;
},
incoming(state) {
acceptIncoming(state, options) {
state.localMediaStream = options.localMediaStream;
},
outgoing(state) {
startRinging(state) {
state.callState = CallState.ringing;
},
establishCall(state, options) {
state.remoteMediaStream = options.remoteMediaStream;
state.callState = CallState.established;
},
incomingCall(state, options) {
state.callState = CallState.incoming;
},
hangUpCall(state) {
state.callState = CallState.input;
if(_.isObject(state.localMediaStream)) {
state.localMediaStream.stop();
state.localMediaStream = null;
}
if(_.isObject(state.remoteMediaStream)) {
state.remoteMediaStream.stop();
state.remoteMediaStream = null;
}
},
endCall(state, reason) {
state.callState = CallState.ended;
state.endedReason = reason;
if(_.isObject(state.localMediaStream)) {
state.localMediaStream.stop();
state.localMediaStream = null;
}
if(_.isObject(state.remoteMediaStream)) {
state.remoteMediaStream.stop();
state.remoteMediaStream = null;
}
}
},
actions: {
initialize(context) {
return new Promise((resolve, reject)=>{
if(context.rootState.user.capabilities.rtcengine) {
loadCdkLib().then((script)=>{
context.commit('load');
return createSessionToken();
}).then((sessionToken)=>{
return connectDefaultCdkNetwork(sessionToken);
}).then(($cdkNetwork)=>{
cdkNetwork = $cdkNetwork;
cdkNetwork.getClient().onConnect(()=>{
context.commit('connect');
Vue.call.initialize().then(()=>{
context.commit('initSucceeded');
Vue.call.onIncoming((call)=>{
context.commit('incomingCall');
call.onRemoteMedia((remoteMediaStream)=>{
context.commit('establishCall', {
remoteMediaStream: remoteMediaStream
});
}).onRemoteMediaEnded(()=>{
context.commit("endRemoteMedia");
}).onEnded(()=>{
context.commit('endCall', call.endedReason);
});
cdkNetwork.getClient().onDisconnect(()=>{
context.commit('disconnect', cdkNetwork.disconnectReason);
});
context.commit('connect');
resolve();
}).catch((err)=>{
context.commit('initFailure');
resolve();
});
} else {
resolve();
}
}).catch((err)=>{
context.commit('initFailed', err);
reject(err);
});
});
},
call(context) {
},
connect(context, sessionToken) {
},
disconnect(context) {
},
enableAudio(context) {
},
disableAudio(context) {
/**
* @param context
* @param options
* @param options.localMedia
* @param options.number
*/
start(context, options) {
Vue.call.createLocalMedia(options.localMedia).then((localMediaStream)=>{
var call = Vue.call.start(options.number, localMediaStream);
call.onAccepted(()=>{
}).onEnded(()=>{
context.commit('endCall', call.endedReason);
}).onPending(()=>{
context.commit('startCalling', {
number: options.number,
mediaType: options.localMedia,
localMediaStream: localMediaStream
});
}).onRemoteMedia((remoteMediaStream)=>{
context.commit('establishCall', {
remoteMediaStream: remoteMediaStream
});
}).onRemoteMediaEnded(()=>{
context.commit("endRemoteMedia");
}).onRingingStart(()=>{
context.commit('startRinging');
}).onRingingStop(()=>{
context.commit('stopRinging');
});
}).catch((err)=>{
context.commit('endCall', err.name);
});
},
enableVideo(context) {
accept(context, localMedia) {
Vue.call.createLocalMedia(localMedia).then((localMediaStream)=>{
Vue.call.accept(localMediaStream);
context.commit('acceptIncoming', {
localMediaStream: localMediaStream
});
}).catch((err)=>{
context.commit('endCall', 'localMediaError');
});
},
disableVideo(context) {
hangUp(context) {
if(Vue.call.hasRunningCall()) {
Vue.call.hangUp();
context.commit('hangUpCall');
} else {
context.commit('endCall', 'noRunningCall');
}
}
}
};

@ -90,9 +90,11 @@ export default {
subscriber: result.subscriber,
capabilities: result.capabilities
});
return context.dispatch('call/initialize', null, { root: true });
}).then(()=>{
resolve();
context.dispatch('call/initialize', null, { root: true }).then(()=>{
resolve();
}).catch((err)=>{
resolve();
});
}).catch((err)=>{
reject(err);
});

@ -17,8 +17,8 @@
// to match your app's branding.
$primary = #66A648
$secondary = #26A69A
$tertiary = #555
$secondary = #32404E
$tertiary = $secondary
$neutral = #E0E1E2
$positive = #21BA45
@ -26,15 +26,14 @@ $negative = #DB2828
$info = #31CCEC
$warning = #F2C037
$toolbar-background = #66A648
$toolbar-background = $primary
$toolbar-min-height = 60px
$layout-header-shadow = $no-shadow
$layout-aside-background = $tertiary
$layout-aside-shadow = $no-shadow
$layout-aside-left-width = 260px
$layout-aside-background = #32404E
$layout-aside-right-width = 320px
$layout-footer-shadow = $no-shadow
$tooltip-background = $primary

Loading…
Cancel
Save