TT#37555 Conversations: Fix and refactor conversation list and infinite scroll

Change-Id: I42751d61c608c40b6a8df50980ea2a172dcedb80
changes/96/21896/3
Hans-Peter Herzog 7 years ago
parent 60fd754bba
commit 858f8121c7

@ -4037,6 +4037,12 @@
}
}
},
"moment": {
"version": "2.22.2",
"from": "moment@latest",
"resolved": "https://npm-registry.sipwise.com/moment/-/moment-2.22.2.tgz",
"dev": true
},
"ms": {
"version": "2.0.0",
"from": "ms@2.0.0",

@ -1,7 +1,7 @@
{
"name": "ngcp-csc-ui",
"productName": "Customer Self-Care Web UI",
"version": "0.2.1",
"version": "0.3.1",
"description": "Customer Self-Care Web UI",
"author": "Hans-Peter Herzog <hherzog@sipwise.com>",
"scripts": {
@ -9,7 +9,8 @@
"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",
"test": "karma start ./karma.js --single-run"
"test": "karma start ./karma.js --single-run",
"dev-test": "karma start ./karma.js"
},
"dependencies": {
"babel-runtime": "^6.25.0",
@ -66,6 +67,7 @@
"load-script": "1.0.0",
"lodash": "4.17.4",
"mocha": "^4.0.0",
"moment": "2.22.2",
"opn": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"postcss-loader": "^1.0.0",

@ -196,7 +196,7 @@ export function addNameIdAndTerminating(options) {
destinationset.timesetId = options.timesetId;
destinationset.destinations.forEach(destination => {
let normalized = normalizeDestination(destination.destination);
if (!terminatingFlag && _.includes(['Voicemail', 'Fax2Mail', 'Manager Secretary',
if (!terminatingFlag && _.includes(['Voicebox', 'Fax2Mail', 'Manager Secretary',
'Custom Announcement', 'Conference'], normalized)) {
terminatingFlag = true;
destination.terminated = false;

@ -1,38 +1,50 @@
import { saveAs } from 'file-saver'
import Vue from 'vue'
import _ from 'lodash'
import crypto from 'crypto-browserify'
import { getJsonBody } from './utils'
import { getList } from './common'
// export function getConversations(id, page, rows) {
// return new Promise((resolve, reject) => {
// let params = { subscriber_id: id, page: page, rows: rows,
// order_by: 'timestamp', order_by_direction: 'desc' };
// Vue.http.get('api/conversations/', { params: params })
// .then(result => {
// let jsonBody = getJsonBody(result.body);
// if (_.has(jsonBody, "_embedded.ngcp:conversations")) {
// let list = [];
// _.forEach(jsonBody._embedded['ngcp:conversations'], function(item) {
// let inputString = `${item.type}${item.call_type}${item.id}`;
// let id = crypto.createHash('sha256').update(inputString).digest('base64');
// item._id = id;
// delete item._links;
// if (item.type == 'call') {
// item.type = item.call_type != 'call' ? 'callforward'
// : item.type;
// }
// list.push(item);
// });
// resolve(list);
// }
// else {
// reject(new Error('No items returned for this page.'))
// }
// }).catch((err) => {
// reject(err);
// });
// });
// }
export function getConversations(id, page, rows) {
return new Promise((resolve, reject) => {
let params = { subscriber_id: id, page: page, rows: rows,
order_by: 'timestamp', order_by_direction: 'desc' };
Vue.http.get('api/conversations/', { params: params })
.then(result => {
let jsonBody = getJsonBody(result.body);
if (_.has(jsonBody, "_embedded.ngcp:conversations")) {
let list = [];
_.forEach(jsonBody._embedded['ngcp:conversations'], function(item) {
let inputString = `${item.type}${item.call_type}${item.id}`;
let id = crypto.createHash('sha256').update(inputString).digest('base64');
item._id = id;
delete item._links;
if (item.type == 'call') {
item.type = item.call_type != 'call' ? 'callforward'
: item.type;
}
list.push(item);
});
resolve(list);
}
else {
reject(new Error('No items returned for this page.'))
}
}).catch((err) => {
reject(err);
});
return getList({
path: 'api/conversations/',
root: '_embedded.ngcp:conversations',
params: {
subscriber_id: id,
page: page,
rows: rows,
order_by: 'timestamp',
order_by_direction: 'desc'
}
});
}

@ -46,7 +46,7 @@
name="videocam" color="primary" size="26px"/>
<q-icon v-else-if="isCalling && (localMediaType == 'audioOnly' || remoteMediaType == 'audioVideo')"
name="mic" color="primary" size="26px"/>
{{ getNumber | numberFormat }}
{{ getNumber | destinationFormat }}
</div>
<div v-if="isEnded" class="ended-reason">{{ getEndedReason }}</div>
</div>

@ -1,33 +1,119 @@
<template>
<csc-page :title="$t('pages.conversations.title')">
<csc-conversations :conversations="conversations" />
<csc-page
ref="page"
class="csc-list-page"
>
<q-list
no-border
inset-separator
sparse
multiline
>
<csc-conversation-item
v-for="(item, index) in items"
:key="item._id"
:item="item"
@init-call="initCall"
@download-fax="downloadFax"
@download-voice-mail="downloadVoiceMail"
@play-voice-mail="playVoiceMail"
/>
</q-list>
<div
v-if="isNextPageRequesting"
class="row justify-center"
>
<q-spinner-dots
color="primary"
:size="40"
/>
</div>
<q-scroll-observable
@scroll="scroll"
/>
</csc-page>
</template>
<script>
import { mapState } from 'vuex'
import {
mapState,
mapGetters
} from 'vuex'
import CscPage from '../../CscPage'
import CscConversations from './CscConversations'
import { startLoading, stopLoading,
showGlobalError, showToast } from '../../../helpers/ui'
import CscConversationItem from './CscConversationItem'
import {
startLoading,
stopLoading,
showGlobalError,
showToast
} from '../../../helpers/ui'
import {
QScrollObservable,
scroll,
QList,
QSpinnerDots
} from 'quasar-framework'
export default {
data () {
return {
scrollEventEmitted: false
}
},
components: {
CscPage,
CscConversations
CscConversationItem,
QScrollObservable,
QList,
QSpinnerDots
},
mounted() {
this.$store.commit('conversations/resetList');
},
computed: {
...mapState('conversations', [
'conversations',
'downloadFaxState',
'downloadVoiceMailState',
'downloadFaxError',
'downloadVoiceMailError'
]),
...mapGetters('conversations', [
'items',
'isNextPageRequesting'
])
},
methods: {
scroll(data) {
if(!this.isNextPageRequesting && !this.scrollEventEmitted && data.direction === 'down' &&
data.position > scroll.getScrollHeight(this.$refs.page.$el) - window.innerHeight + 30) {
this.scrollEventEmitted = true;
this.nextPage();
}
else if(data.position <= scroll.getScrollHeight(this.$refs.page.$el) - window.innerHeight + 30) {
this.scrollEventEmitted = false;
}
},
nextPage() {
this.$store.dispatch('conversations/nextPage');
},
initCall(call) {
this.$store.dispatch('call/start', {
number: call.number,
localMedia: call.media
});
},
downloadFax(fax) {
this.$store.dispatch('conversations/downloadFax', fax.id);
},
downloadVoiceMail(voiceMail) {
this.$store.dispatch('conversations/downloadVoiceMail', voiceMail.id);
},
playVoiceMail(voiceMail) {
this.$store.dispatch('conversations/playVoiceMail', {
id: voiceMail.id,
format: voiceMail.format
});
}
},
watch: {
downloadVoiceMailState(state) {
if (state === 'requesting') {
@ -62,4 +148,15 @@
<style lang="stylus" rel="stylesheet/stylus">
.q-infinite-scroll-message
margin-bottom 50px
.csc-voice-mail-item
.csc-item-buttons
position absolute
right 16px
.csc-item-buttons
.q-btn
padding-left 8px;
padding-right 8px;
</style>

@ -0,0 +1,160 @@
<template>
<q-item
class="csc-entity csc-call-item"
>
<q-item-side
:icon="icon"
:color="color"
/>
<q-item-main>
<q-item-tile
label
>
<span class="gt-sm csc-entity-title">{{ typeTerm }}</span>
<span class="gt-sm csc-entity-title">{{ direction }}</span>
<span class="csc-entity-title">{{ number | destinationFormat }}</span>
</q-item-tile>
<q-item-tile
sublabel
>{{ call.start_time | smartTime }}
</q-item-tile>
</q-item-main>
<q-item-side
right
class="csc-item-buttons"
>
<q-item-tile>
<q-btn
icon="call"
color="primary"
slot="right"
flat
>
<q-popover ref="callPopover" anchor="bottom right" self="top right">
<csc-call-option-list
ref="callOptionPopover"
@init-call="initCall"
/>
</q-popover>
</q-btn>
</q-item-tile>
</q-item-side>
</q-item>
</template>
<script>
import _ from 'lodash'
import CscCallOptionList from './CscCallOptionList'
import {
QItem,
QItemSide,
QItemMain,
QItemTile,
QBtn,
QPopover,
QList,
Platform
} from 'quasar-framework'
export default {
name: 'csc-call-item',
props: [
'call'
],
components: {
QItem,
QItemSide,
QItemMain,
QItemTile,
QBtn,
QPopover,
QList,
CscCallOptionList
},
data () {
return {}
},
computed: {
number() {
if(this.call.direction === 'out') {
return this.call.callee;
}
else {
return this.call.caller;
}
},
numberDialBack() {
if (_.isObject(this.call.relatedCall)) {
return this.call.relatedCall.caller;
}
else if(this.call.direction === 'out') {
return this.call.callee;
}
else {
return this.call.caller;
}
},
direction() {
if(this.call.direction === 'out') {
return 'to';
}
else {
return 'from';
}
},
typeTerm() {
if(this.call.call_type === 'call') {
return 'Call';
}
else {
return 'Call forwarded'
}
},
icon() {
if(this.call.call_type === 'cfu' || this.call.call_type === 'cfna' ||
this.call.call_type === 'cfb' && this.call.call_type === 'cft') {
return 'phone_forwarded';
}
else if (this.call.call_type === 'call' && this.call.direction === 'in' && this.call.status === 'cancel') {
return 'call_missed';
}
else if (this.call.call_type === 'call' && this.call.direction === 'in') {
return 'call_received';
}
else if (this.call.call_type === 'call' && this.call.direction === 'out') {
return 'call_made';
}
else {
return 'phone';
}
},
color() {
if (this.call.call_type === 'call' && (this.call.status === 'cancel' ||
this.call.status === 'offline' || this.call.status === 'noanswer')) {
return 'negative';
}
else if (this.call.call_type === 'call' && (this.call.direction === 'in' ||
this.call.direction === 'out')) {
return 'primary';
}
else {
return 'default';
}
},
isMobile() {
return Platform.is.mobile;
}
},
methods: {
initCall(media) {
this.$refs.callPopover.close();
this.$emit('init-call', {
media: media,
number: this.numberDialBack
});
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -0,0 +1,52 @@
<template>
<q-list item-separator link class="csc-toolbar-btn-popover">
<q-item @click="initCall('audioOnly')">
<q-item-side icon="mic" color="primary" />
<q-item-main :label="$t('startAudioCall')" />
</q-item>
<q-item @click="initCall('audioVideo')">
<q-item-side icon="videocam" color="primary" />
<q-item-main :label="$t('startVideoCall')" />
</q-item>
<q-item @click="initCall('audioScreen')">
<q-item-side icon="computer" color="primary" />
<q-item-main :label="$t('startScreenSharing')" />
</q-item>
</q-list>
</template>
<script>
import CscCallOptionList from './CscCallOptionList'
import {
QItem,
QItemSide,
QItemMain,
QItemTile,
QPopover,
QList
} from 'quasar-framework'
export default {
name: 'csc-call-option-list',
data () {
return {}
},
components: {
QItem,
QItemSide,
QItemMain,
QItemTile,
QPopover,
QList,
CscCallOptionList
},
methods: {
initCall(media) {
this.$emit('init-call', media);
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -1,161 +0,0 @@
<template>
<csc-card-collapsible :list-item="conversation" :collapsible="hasCollapsibleData"
:firstIcon="getFirstIcon()" :secondIcon="getSecondIcon()"
:sublabel="conversation.start_time | readableDate" :id="conversation._id">
<span slot="title">
{{ getTitle() }}
<span v-if="isType('fax')" style="padding-left:12px;">
<q-chip pointing="left" color="primary" class="csc-number-chip">
<strong>Pages:</strong>
{{ conversation.pages }}
</q-chip>
</span>
</span>
<div v-if="isType('call')" slot="main">
<ul>
<li>
<strong>
{{ $t('pages.conversations.card.duration') }}:
</strong> {{ conversation.duration }}
</li>
</ul>
</div>
<div v-else-if="isType('voicemail')" slot="main">
<ul>
<li>
<strong>
{{ $t('pages.conversations.card.duration') }}:
</strong> {{ conversation.duration }}
</li>
<li>
<strong>
{{ $t('pages.conversations.card.folder') }}:
</strong> {{ conversation.folder }}
</li>
</ul>
</div>
<div v-if="!isType('fax')" slot="footer">
<q-card-separator />
<csc-voicemail-player id="csc-voicemail-player" v-if="isType('voicemail')" :id="conversation.id" />
<q-card-separator />
<q-card-actions align="center">
<q-btn flat round small color="primary" icon="call">
{{ $t('pages.conversations.buttons.call') }}
<q-popover ref="popover">
<q-list separator link>
<q-item @click="call('audioOnly'),
$refs.popover.close()">
{{ $t('pages.conversations.buttons.audioCall') }}
</q-item>
<q-item @click="call('audioVideo'),
$refs.popover.close()">
{{ $t('pages.conversations.buttons.videoCall') }}
</q-item>
</q-list>
</q-popover>
</q-btn>
<q-btn v-if="isType('voicemail')" flat round small color="primary"
icon="get_app" @click="downloadVoiceMail(conversation.id)">
{{ $t('pages.conversations.buttons.download') }}
</q-btn>
</q-card-actions>
</div>
<div v-else-if="isType('fax')" slot="footer">
<q-card-separator />
<q-card-actions align="center">
<q-btn flat round small color="primary"
icon="file_download" @click="downloadFax(conversation.id)">
{{ $t('pages.conversations.buttons.download') }}
</q-btn>
</q-card-actions>
</div>
</csc-card-collapsible>
</template>
<script>
import CscCardCollapsible from '../../card/CscCardCollapsible'
import CscVoicemailPlayer from './CscVoicemailPlayer'
import { QBtn, QPopover, QItem, QList, QCardActions,
QChip, QCardSeparator } from 'quasar-framework'
import numberFormat from '../../../filters/number-format'
export default {
name: 'csc-conversation',
props: [
'conversation'
],
components: {
QBtn,
QPopover,
QItem,
QList,
QChip,
QCardSeparator,
QCardActions,
CscCardCollapsible,
CscVoicemailPlayer
},
computed: {
hasCollapsibleData() {
return (['call', 'voicemail'].indexOf(this.conversation.type) > -1);
}
},
methods: {
downloadVoiceMail(id) {
this.$store.dispatch('conversations/downloadVoiceMail', id);
},
downloadFax(id) {
this.$store.dispatch('conversations/downloadFax', id);
},
call(localMedia) {
let conversation = this.conversation;
let number = conversation.direction == 'out' ?
conversation.callee : conversation.caller;
this.$store.dispatch('call/start',
{ number: number, localMedia: localMedia });
},
getFirstIcon() {
let conversation = this.conversation;
switch (conversation.type) {
case 'call':
return 'phone';
case 'callforward':
return 'call_merge';
case 'voicemail':
return 'voicemail';
case 'fax':
return 'insert_drive_file';
case 'sms':
return 'txtsms';
}
},
getSecondIcon() {
let conversation = this.conversation;
return conversation.direction == 'out' ? 'call_made' : 'call_received';
},
getTitle() {
let conversation = this.conversation;
let prefix;
if (!conversation.status || ['ok', 'SUCCESS'].indexOf(conversation.status) > -1) {
prefix = this.$t('pages.conversations.labels.successful');
}
else {
prefix = this.$t('pages.conversations.labels.unsuccessful');
}
let direction = conversation.direction == 'in' ?
this.$t('pages.conversations.labels.from') :
this.$t('pages.conversations.labels.to');
let number = conversation.caller;
if(conversation.direction === 'out') {
number = conversation.callee;
}
return `${prefix} ${conversation.type} ${direction} ${numberFormat(number)}`;
},
isType(type) {
return this.conversation.type == type;
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -0,0 +1,58 @@
<template>
<csc-call-item
v-if="item.type == 'call'"
:call="item"
@init-call="initCall"
/>
<csc-fax-item
v-else-if="item.type == 'fax'"
:fax="item"
@init-call="initCall"
@download-fax="downloadFax"
/>
<csc-voice-mail-item
v-else-if="item.type == 'voicemail'"
:voice-mail="item"
@init-call="initCall"
@download-voice-mail="downloadVoiceMail"
@play-voice-mail="playVoiceMail"
/>
</template>
<script>
import CscCallItem from './CscCallItem'
import CscFaxItem from './CscFaxItem'
import CscVoiceMailItem from './CscVoiceMailItem'
export default {
name: 'csc-conversation-item',
props: [
'item'
],
components: {
CscCallItem,
CscFaxItem,
CscVoiceMailItem
},
mounted() {},
data () {
return {}
},
methods: {
initCall(call) {
this.$emit('init-call', call);
},
downloadFax(fax) {
this.$emit('download-fax', fax);
},
downloadVoiceMail(voiceMail) {
this.$emit('download-voice-mail', voiceMail);
},
playVoiceMail(voiceMail) {
this.$emit('play-voice-mail', voiceMail);
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -1,71 +0,0 @@
<template>
<q-infinite-scroll
:handler="loadMore"
:offset=1
ref="infinite"
>
<csc-conversation
v-for="(conversation, index) in conversations"
:conversation="conversation"
:key="conversation._id"
/>
<div
slot="message"
class="row justify-center"
>
<q-spinner-dots :size="40" />
</div>
</q-infinite-scroll>
</template>
<script>
import CscConversation from './CscConversation'
import { mapGetters } from 'vuex'
import {
QInfiniteScroll,
QSpinnerDots
} from 'quasar-framework'
export default {
name: 'csc-conversations',
props: ['conversations'],
components: {
CscConversation,
QInfiniteScroll,
QSpinnerDots
},
computed: {
...mapGetters('call', [
'callState'
])
},
methods: {
reloadConversations() {
this.$store.dispatch('conversations/reloadConversations', 1);
this.$refs.infinite.reset();
this.$refs.infinite.resume();
},
loadMore(index, done) {
this.$store.dispatch('conversations/loadConversations')
.then(() => {
done();
}).catch(() => {
done();
this.$refs.infinite.stop();
});
}
},
watch: {
callState(newState, oldState) {
let endedA = newState === 'ended';
let endedB = oldState === 'established' && newState === 'input';
let endedC = oldState === 'ringing' && newState === 'input';
if (endedA || endedB || endedC) {
this.reloadConversations();
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -0,0 +1,107 @@
<template>
<q-item class="csc-entity csc-fax-item">
<q-item-side
icon="description"
color="primary"
/>
<q-item-main>
<q-item-tile
label
>
<span class="gt-sm csc-entity-title">Fax from </span>
<span class="csc-entity-title">{{ fax.caller | numberFormat }}</span>
</q-item-tile>
<q-item-tile
sublabel
>{{ fax.start_time | smartTime }}
</q-item-tile>
<q-item-tile
v-if="fax.pages === 0"
sublabel
>No pages</q-item-tile>
<q-item-tile
v-else-if="fax.pages === 1"
sublabel
>{{ fax.pages }} page
</q-item-tile>
<q-item-tile
v-else
sublabel
>{{ fax.pages }} pages
</q-item-tile>
</q-item-main>
<q-item-side
right
class="csc-item-buttons"
>
<q-item-tile>
<q-btn
icon="file_download"
color="primary"
slot="right"
flat
@click="downloadFax"
>
</q-btn>
<q-btn
icon="call"
color="primary"
slot="right"
flat
>
<q-popover ref="callPopover" anchor="bottom right" self="top right">
<csc-call-option-list
ref="callOptionPopover"
@init-call="initCall"
/>
</q-popover>
</q-btn>
</q-item-tile>
</q-item-side>
</q-item>
</template>
<script>
import CscCallOptionList from './CscCallOptionList'
import {
QItem,
QItemSide,
QItemMain,
QItemTile,
QPopover,
QBtn
} from 'quasar-framework'
export default {
name: 'csc-fax-item',
props: [
'fax'
],
components: {
QItem,
QItemSide,
QItemMain,
QItemTile,
QPopover,
QBtn,
CscCallOptionList
},
data () {
return {}
},
methods: {
initCall(media) {
this.$refs.callPopover.close();
this.$emit('init-call', {
media: media,
number: this.fax.caller
});
},
downloadFax() {
this.$emit('download-fax', this.fax);
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -0,0 +1,116 @@
<template>
<q-item
class="csc-entity csc-voice-mail-item"
>
<q-item-side
color="primary"
icon="voicemail"
/>
<q-item-main>
<q-item-tile
label
>
<span class="gt-sm csc-entity-title">Voicemail from </span>
<span class="csc-entity-title">{{ voiceMail.caller | numberFormat }}</span>
</q-item-tile>
<q-item-tile
sublabel
>{{ voiceMail.start_time | smartTime }}
</q-item-tile>
<q-item-tile
sublabel
>Duration: {{ voiceMail.duration }} seconds
</q-item-tile>
<q-item-tile>
<csc-voice-mail-player
:id="voiceMail.id"
class="csc-voice-mail-player"
@play-voice-mail="playVoiceMail"
/>
</q-item-tile>
</q-item-main>
<q-item-side
right
class="csc-item-buttons"
>
<q-item-tile>
<q-btn
icon="file_download"
color="primary"
slot="right"
flat
@click="downloadVoiceMail"
>
</q-btn>
<q-btn
icon="call"
color="primary"
slot="right"
flat
>
<q-popover ref="callPopover" anchor="bottom right" self="top right">
<csc-call-option-list
ref="callOptionPopover"
@init-call="initCall"
/>
</q-popover>
</q-btn>
</q-item-tile>
</q-item-side>
</q-item>
</template>
<script>
import CscCallOptionList from './CscCallOptionList'
import CscVoiceMailPlayer from './CscVoiceMailPlayer'
import {
QItem,
QItemSide,
QItemMain,
QItemTile,
QPopover,
QBtn
} from 'quasar-framework'
export default {
name: 'csc-voice-mail-item',
props: [
'voiceMail'
],
components: {
QItem,
QItemSide,
QItemMain,
QItemTile,
QPopover,
QBtn,
CscVoiceMailPlayer,
CscCallOptionList
},
data () {
return {}
},
methods: {
initCall(media) {
this.$refs.callPopover.close();
this.$emit('init-call', {
media: media,
number: this.voiceMail.callee
});
},
playVoiceMail() {
this.$emit('play-voice-mail', this.voiceMail);
},
downloadVoiceMail() {
this.$emit('download-voice-mail', this.voiceMail);
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.csc-voice-mail-player
padding 0
margin-top 16px
</style>

@ -1,21 +1,22 @@
<template>
<div class="voicemail-player">
<audio :src="soundFileUrl" ref="voiceMailSound" preload="none" />
<audio :src="soundFileUrl" ref="voiceMailSound" preload="auto" @timeupdate="timeupdate($event)"/>
<div class="control-btns">
<q-btn class="play-pause-btn" round flat small color="primary"
:icon="playPauseIcon" @click="toggle()" />
<q-btn class="stop-btn" round flat small color="primary" icon="stop" />
<q-btn class="stop-btn" round flat small color="primary" icon="stop" @click="stop()"/>
</div>
<q-progress class="progress-bar" :indeterminate="isLoading" stripe animate color="primary"/>
<q-progress class="progress-bar" :percentage="progressPercentage" stripe animate color="primary"/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { QProgress, QBtn } from 'quasar-framework'
export default {
name: 'csc-voicemail-player',
name: 'csc-voice-mail-player',
props: {
id: Number
},
@ -28,9 +29,12 @@
});
this.$refs.voiceMailSound.addEventListener('ended', ()=>{
this.playing = false;
this.stop();
});
this.$refs.voiceMailSound.addEventListener('canplay', ()=>{
this.$refs.voiceMailSound.play();
if(!this.paused && this.playing) {
this.$refs.voiceMailSound.play();
}
});
},
components: {
@ -39,10 +43,10 @@
},
data () {
return {
progress: 77,
platform: this.$q.platform.is,
voicemail: null,
playing: false
playing: false,
paused: false,
progressPercentage: 0
}
},
computed: {
@ -56,29 +60,33 @@
let getter = this.playVoiceMailUrl;
return getter(this.id);
},
isLoading() {
let getter = this.playVoiceMailState;
return getter(this.id) === 'requesting';
},
...mapGetters('conversations', [
'playVoiceMailState',
'playVoiceMailUrl'
]),
])
},
methods: {
play() {
this.$refs.voiceMailSound.play();
this.playing = true;
this.paused = false;
},
pause() {
this.$refs.voiceMailSound.pause();
this.playing = false;
this.paused = true;
},
stop() {
this.$refs.voiceMailSound.currentTime = 0;
this.pause();
},
load() {
this.$store.dispatch('conversations/playVoiceMail', {
this.$emit('play-voice-mail', {
id: this.id,
format: this.soundFileFormat
});
this.playing = true;
this.paused = false;
},
toggle() {
if(this.playVoiceMailState(this.id) !== 'succeeded') {
@ -90,6 +98,10 @@
else {
this.pause();
}
},
timeupdate(e) {
let newPercentage = Math.floor((e.target.currentTime / e.target.duration) * 100);
this.progressPercentage = newPercentage;
}
}
}
@ -104,8 +116,6 @@
display flex
justify-content space-around
align-items center
padding-left 16px
padding-right 16px
border-radius 4px
background-color #fff
.control-btns

@ -100,7 +100,7 @@
import CscPage from '../CscPage'
import CscCall from '../CscCall'
import { mapGetters } from 'vuex'
import { QCard, QCardMain, QCardActions, QIcon, Platform } from 'quasar-framework'
import { QCard, QCardMain, QCardActions, QIcon, Platform, scroll } from 'quasar-framework'
import { showGlobalWarning } from '../../helpers/ui'
export default {
@ -116,6 +116,9 @@
QCardActions,
QIcon
},
mounted() {
scroll.setScrollPosition(this.$el, 0, 100);
},
computed: {
...mapGetters('call', [
'isCallAvailable'

@ -283,6 +283,6 @@
.csc-item-buttons
.q-btn
padding-left 8px;
padding-right 8px;
padding-left 8px
padding-right 8px
</style>

@ -1,4 +1,6 @@
import { isYesterday, isToday, isWithinLastWeek } from '../helpers/date-helper'
import moment from 'moment'
import { date } from 'quasar-framework'
const { formatDate } = date;
@ -6,3 +8,41 @@ export default function(value) {
var timeStamp = new Date(value);
return `${formatDate(timeStamp, 'MMMM D, YYYY')} at ${formatDate(timeStamp, 'h:mm a')}`;
}
export function smartTime($date, $today) {
let today = $today || new Date();
let date = new Date($date);
let diffSeconds = Math.floor((today.getTime() - date.getTime()) / 1000);
let diffMinutes = Math.floor(diffSeconds / 60);
let momentDate = moment(date);
let seconds = 'second';
if(diffSeconds > 1) {
seconds = seconds + "s";
}
let minutes = 'minute';
if(diffSeconds > 120) {
minutes = minutes + "s";
}
if(diffSeconds < 60) {
return diffSeconds + ' ' + seconds + ' ago';
}
else if (diffSeconds < 3600) {
return diffMinutes + ' ' + minutes + ' ago';
}
else if(isToday(date)) {
return 'Today, ' + momentDate.format('HH:mm');
}
else if (isYesterday(date)) {
return 'Yesterday, ' + momentDate.format('HH:mm');
}
else if (isWithinLastWeek(date)) {
return momentDate.format('dddd, HH:mm');
}
else {
return momentDate.format('LLL');
}
}

@ -4,8 +4,10 @@ import NumberFilter from './number'
import NumberFormatFilter from './number-format'
import { normalizeDestination } from './number-format'
import DateFilter from './date'
import { smartTime } from './date'
Vue.filter('number', NumberFilter);
Vue.filter('readableDate', DateFilter);
Vue.filter('numberFormat', NumberFormatFilter);
Vue.filter('destinationFormat', normalizeDestination);
Vue.filter('smartTime', smartTime);

@ -1,10 +1,20 @@
import _ from 'lodash';
import url from 'url';
import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber';
import {
PhoneNumberUtil,
PhoneNumberFormat
} from 'google-libphonenumber';
var phoneUtil = PhoneNumberUtil.getInstance();
const DestinationHosts = {
VoiceBox: 'voicebox.local',
Fax2Mail: 'fax2mail.local',
ManagerSecretary: 'managersecretary.local',
App: 'app.local'
};
export default function numberFormat(number) {
try {
let destination = url.parse(number, true);
@ -57,24 +67,32 @@ export function rawNumber(number) {
export function normalizeDestination(destination) {
try {
destination = destination.replace(/\s*/g, '');
if(destination.match(/^sip:/g) === null && destination.match(/^sips:/g) === null &&
destination.match(/^\+?[0-9]+$/) === null) {
destination = 'sip:' + destination;
}
let parsedDestination = url.parse(destination, true);
let authParts = parsedDestination.auth.split(':');
let host = parsedDestination.host;
let normalizedNumber = normalizeNumber(authParts[0]);
let isNumber = normalizedNumber !== authParts[0];
if (host === 'voicebox.local') {
return 'Voicemail';
if (host === DestinationHosts.VoiceBox) {
return 'Voicebox';
}
else if (host === 'fax2mail.local') {
else if (host === DestinationHosts.Fax2Mail) {
return 'Fax2Mail';
}
else if (host === 'managersecretary.local') {
else if (host === DestinationHosts.ManagerSecretary) {
return 'Manager Secretary';
}
else if (authParts[0] === 'custom-hours') {
return 'Custom Announcement';
}
else if (host === 'app.local') {
else if (host === DestinationHosts.App) {
return _.capitalize(authParts[0]);
}
else if (!isNumber) {

@ -0,0 +1,85 @@
export function addSecond(date) {
let newDate = new Date();
newDate.setTime(date.getTime() + 1000);
return newDate;
}
export function addMinute(date) {
let newDate = new Date();
newDate.setTime(date.getTime() + 60000);
return newDate;
}
export function addHour(date) {
let newDate = new Date();
newDate.setTime(date.getTime() + 3600000);
return newDate;
}
export function addDay(date) {
let newDate = new Date();
newDate.setTime(date.getTime() + 86400000);
return newDate;
}
export function addMonth(date) {
let newDate = new Date();
newDate.setUTCMonth(date.getUTCMonth() + 1);
return newDate;
}
export function addYear(date) {
let newDate = new Date();
newDate.setUTCFullYear(date.getUTCFullYear() + 1);
return newDate;
}
export function isToday(date, $today) {
let today = $today || new Date();
let todayStart = new Date(today.getTime());
let tomorrowStart = new Date(today.getTime() + 86400000);
tomorrowStart.setHours(0);
tomorrowStart.setMinutes(0);
tomorrowStart.setSeconds(0);
tomorrowStart.setMilliseconds(0);
todayStart.setHours(0);
todayStart.setMinutes(0);
todayStart.setSeconds(0);
todayStart.setMilliseconds(0);
return date.getTime() >= todayStart.getTime() &&
date.getTime() < tomorrowStart.getTime();
}
export function isYesterday(yesterday, $today) {
let today = $today || new Date();
let yesterdayStart = new Date(today.getTime() - 86400000);
let todayStart = new Date(today.getTime());
yesterdayStart.setHours(0);
yesterdayStart.setMinutes(0);
yesterdayStart.setSeconds(0);
yesterdayStart.setMilliseconds(0);
todayStart.setHours(0);
todayStart.setMinutes(0);
todayStart.setSeconds(0);
todayStart.setMilliseconds(0);
return yesterday.getTime() >= yesterdayStart.getTime() &&
yesterday.getTime() < todayStart.getTime();
}
export function isWithinLastWeek(date, $today) {
let today = $today || new Date();
let weekStart = new Date(today.getTime() - 86400000 * 6);
let todayStart = new Date(today.getTime());
weekStart.setHours(0);
weekStart.setMinutes(0);
weekStart.setSeconds(0);
weekStart.setMilliseconds(0);
todayStart.setHours(0);
todayStart.setMinutes(0);
todayStart.setSeconds(0);
todayStart.setMilliseconds(0);
return date.getTime() >= weekStart.getTime() &&
date.getTime() < todayStart.getTime();
}

@ -1,7 +1,10 @@
{
"title": "Customer Self-Care Portal",
"rtcEngineDisconnected": "You can not start a call. Service ist currently unavailable.",
"startCall": "Start Call",
"startCall": "Call",
"startAudioCall": "Audio Only",
"startVideoCall": "Audio + Video",
"startScreenSharing": "Audio + Screen",
"sendSms": "Send SMS",
"sendFax": "Send Fax",
"loggedInAs": "Logged in as",

@ -1,7 +1,6 @@
'use strict';
import Vue from 'vue'
import _ from 'lodash';
import { i18n } from '../i18n';
import {
getConversations,
@ -10,6 +9,8 @@ import {
playVoiceMail
} from '../api/conversations'
const ROWS_PER_PAGE = 15;
const RequestState = {
button: 'button',
requesting: 'requesting',
@ -17,18 +18,12 @@ const RequestState = {
failed: 'failed'
};
const ReloadConfig = {
retryLimit: 5,
retryDelay: 5000
};
export default {
namespaced: true,
state: {
page: 1,
rows: 10,
conversations: [
],
conversations: [],
downloadVoiceMailState: RequestState.button,
downloadVoiceMailError: null,
downloadFaxState: RequestState.button,
@ -37,7 +32,12 @@ export default {
reloadConversationsError: null,
playVoiceMailUrls: {},
playVoiceMailStates: {},
playVoiceMailErrors: {}
playVoiceMailErrors: {},
currentPage: 0,
lastPage: null,
nextPageState: RequestState.initiated,
nextPageError: null,
items: []
},
getters: {
getSubscriberId(state, getters, rootState, rootGetters) {
@ -59,6 +59,21 @@ export default {
return (id) => {
return state.playVoiceMailUrls[id];
}
},
currentPage(state) {
return state.currentPage;
},
lastPage(state) {
return state.lastPage;
},
rowsAlreadyLoaded(state) {
return state.items.length;
},
items(state) {
return state.items;
},
isNextPageRequesting(state) {
return state.nextPageState === RequestState.requesting;
}
},
mutations: {
@ -122,46 +137,45 @@ export default {
Vue.set(state.playVoiceMailUrls, id, null);
Vue.set(state.playVoiceMailStates, id, RequestState.failed);
Vue.set(state.playVoiceMailErrors, id, err);
}
},
actions: {
reloadConversations(context, retryCount) {
context.commit('resetConversations');
let firstItem = context.state.conversations[0];
if (retryCount < ReloadConfig.retryLimit) {
getConversations({
id: context.getters.getSubscriberId,
page: context.state.page,
rows: context.state.rows
}).then((result) => {
if (_.isEqual(firstItem, result[0])) {
setTimeout(() => {
context.dispatch('reloadConversations', ++retryCount);
}, ReloadConfig.retryDelay);
}
else {
context.commit('reloadConversations', result);
context.commit('reloadConversationsSucceeded');
}
}).catch(() => {
context.commit('reloadConversationsFailed');
});
}
},
loadConversations(context) {
return new Promise((resolve, reject) => {
getConversations({
id: context.getters.getSubscriberId,
page: context.state.page,
rows: context.state.rows
}).then((result) => {
context.commit('loadConversations', result);
resolve();
}).catch((err)=>{
reject(err);
});
resetList(state) {
state.items = [];
state.currentPage = 0;
},
nextPageRequesting(state) {
state.nextPageState = RequestState.requesting;
state.nextPageError = null;
},
nextPageSucceeded(state, items) {
state.nextPageState = RequestState.succeeded;
state.nextPageError = null;
state.items = state.items.concat(items.items);
state.lastPage = items.lastPage;
state.currentPage = state.currentPage + 1;
let callId = null;
let callIndex = null;
state.items.forEach((item, index)=>{
if(item.type === 'call' && item.call_type === 'call') {
callId = item.call_id;
callIndex = index;
}
else if (item.type === 'call' && item.call_id === callId) {
let temp = state.items[callIndex];
item.relatedCall = temp;
state.items[callIndex] = item;
state.items[index] = temp;
callIndex = index;
}
});
},
nextPageFailed(state, error) {
state.nextPageState = RequestState.failed;
state.nextPageError = error;
}
},
actions: {
downloadVoiceMail(context, id) {
context.commit('downloadVoiceMailRequesting');
downloadVoiceMail(id).then(()=>{
@ -188,6 +202,21 @@ export default {
}).catch((err)=>{
context.commit('playVoiceMailFailed', options.id, err.mesage);
});
},
nextPage(context) {
let page = context.getters.currentPage + 1;
if(context.getters.lastPage === null || page <= context.getters.lastPage) {
context.commit('nextPageRequesting');
getConversations(
context.getters.getSubscriberId,
page,
ROWS_PER_PAGE
).then((result) => {
context.commit('nextPageSucceeded', result);
}).catch((err)=>{
context.commit('nextPageFailed', err.message);
});
}
}
}
};

@ -19,6 +19,14 @@
.csc-entity
position relative
.csc-entity-title {
color $tertiary
font-size 18px
font-weight 400
letter-spacing normal
line-height 1.8rem
}
.q-btn
.on-left
margin 0

@ -5,7 +5,7 @@ FROM docker.mgm.sipwise.com/sipwise-stretch:latest
# is updated with the current date. It will force refresh of all
# of the base images and things like `apt-get update` won't be using
# old cached versions when the Dockerfile is built.
ENV REFRESHED_AT 2018-05-24
ENV REFRESHED_AT 2018-06-14
ENV DEBIAN_FRONTEND noninteractive
ENV DISPLAY=:0

@ -14,8 +14,6 @@ describe('Conversations', function(){
const subscriberId = 123;
it('should get all data regarding conversations', function(done){
let inputString = 'voicemailundefined1';
let hashedId = crypto.createHash('sha256').update(inputString).digest('base64');
let innerData = [{
"_links" : {
@ -60,24 +58,52 @@ describe('Conversations', function(){
let data = {
"_embedded": {
"ngcp:conversations": innerData
}
},
total_count: 1
};
let innerDataWithoutLinks = [{
"call_id": "kp55kEGtNp",
"callee": "43993006",
"caller": "43993006",
"context": "voicemailcaller_unavail",
"direction": "in",
"duration": "15",
"filename": "voicemail-0.wav",
"folder": "Old",
"id": 1,
"start_time": "2017-12-07 16:22:04",
"type": "voicemail",
"voicemail_subscriber_id": 235,
"_id": hashedId
}];
let innerDataTransformed = {
items: [{
"_links" : {
"collection": {
"href": "/api/conversations/"
},
"curies": {
"href": "http://purl.org/sipwise/ngcp-api/#rel-{rel}",
"name": "ngcp",
"templated": true
},
"ngcp:conversations": {
"href": "/api/conversations/1?type=voicemail"
},
"ngcp:voicemailrecordings": {
"href": "/api/voicemailrecordings/1"
},
"ngcp:voicemails": {
"href": "/api/voicemails/1"
},
"profile": {
"href": "http://purl.org/sipwise/ngcp-api/"
},
"self": {
"href": "/api/conversations/1?type=voicemail"
}
},
"call_id": "kp55kEGtNp",
"callee": "43993006",
"caller": "43993006",
"context": "voicemailcaller_unavail",
"direction": "in",
"duration": "15",
"filename": "voicemail-0.wav",
"folder": "Old",
"id": 1,
"start_time": "2017-12-07 16:22:04",
"type": "voicemail",
"voicemail_subscriber_id": 235
}],
lastPage: 1
};
Vue.http.interceptors = [];
Vue.http.interceptors.unshift((request, next)=>{
@ -86,7 +112,7 @@ describe('Conversations', function(){
}));
});
getConversations(subscriberId).then((result)=>{
assert.deepEqual(result, innerDataWithoutLinks);
assert.deepEqual(result, innerDataTransformed);
done();
}).catch((err)=>{
done(err);

@ -0,0 +1,74 @@
'use strict';
import { assert } from 'chai';
import { isYesterday, isToday, isWithinLastWeek } from '../../src/helpers/date-helper'
describe('Date helper', function() {
it('should check whether a given date is yesterday or not', function() {
let today = new Date('2000-01-01 00:00:00');
let beforeYesterday = new Date('1999-12-30 00:00:00');
let tomorrow = new Date('2000-01-02 00:00:00');
let yesterday1 = new Date('1999-12-31 00:00:00');
let yesterday2 = new Date('1999-12-31 14:00:00');
let yesterday3 = new Date('1999-12-31 23:59:59');
assert.isTrue(isYesterday(yesterday1, today));
assert.isTrue(isYesterday(yesterday2, today));
assert.isTrue(isYesterday(yesterday3, today));
assert.isFalse(isYesterday(beforeYesterday, today));
assert.isFalse(isYesterday(today, today));
assert.isFalse(isYesterday(tomorrow, today));
});
it('should check whether a given date is today or not', function() {
let today = new Date('2000-01-01 00:00:00');
let yesterday = new Date('1999-12-31 00:00:00');
let beforeYesterday = new Date('1999-12-30 00:00:00');
let tomorrow = new Date('2000-01-02 00:00:00');
let afterTomorrow = new Date('2000-01-03 00:00:00');
let today1 = new Date('2000-01-01 00:00:00');
let today2 = new Date('2000-01-01 14:00:00');
let today3 = new Date('2000-01-01 23:59:59');
assert.isTrue(isToday(today, today));
assert.isTrue(isToday(today1, today));
assert.isTrue(isToday(today2, today));
assert.isTrue(isToday(today3, today));
assert.isFalse(isToday(beforeYesterday, today));
assert.isFalse(isToday(yesterday, today));
assert.isFalse(isToday(tomorrow, today));
assert.isFalse(isToday(afterTomorrow, today));
});
it('should check whether a given date is within last week or not', function(){
let today = new Date('2000-01-01 00:00:00');
let validDay1 = new Date('1999-12-31 00:00:00');
let validDay2 = new Date('1999-12-30 00:00:00');
let validDay3 = new Date('1999-12-29 00:00:00');
let validDay4 = new Date('1999-12-28 00:00:00');
let validDay5 = new Date('1999-12-27 00:00:00');
let validDay6 = new Date('1999-12-26 00:00:00');
let invalidDay1 = new Date('1999-12-25 00:00:00');
let invalidDay2 = new Date('1999-12-24 00:00:00');
let invalidDay3 = new Date('1999-12-23 00:00:00');
assert.isTrue(isWithinLastWeek(validDay1, today));
assert.isTrue(isWithinLastWeek(validDay2, today));
assert.isTrue(isWithinLastWeek(validDay3, today));
assert.isTrue(isWithinLastWeek(validDay4, today));
assert.isTrue(isWithinLastWeek(validDay5, today));
assert.isTrue(isWithinLastWeek(validDay6, today));
assert.isFalse(isWithinLastWeek(today, today));
assert.isFalse(isWithinLastWeek(invalidDay1, today));
assert.isFalse(isWithinLastWeek(invalidDay2, today));
assert.isFalse(isWithinLastWeek(invalidDay3, today));
});
});

@ -57,7 +57,7 @@ describe('NumberFormatFilter', function() {
});
it('should format a call forward destination', function(){
assert.equal(normalizeDestination(destinations.voiceMail), 'Voicemail');
assert.equal(normalizeDestination(destinations.voiceMail), 'Voicebox');
assert.equal(normalizeDestination(destinations.fax2Mail), 'Fax2Mail');
assert.equal(normalizeDestination(destinations.managerSecretary), 'Manager Secretary');
assert.equal(normalizeDestination(destinations.app), 'App');

Loading…
Cancel
Save