mirror of https://github.com/sipwise/ngcp-csc.git
Change-Id: Ib5bb129553c0433204449f6bf93e14e73069973dchanges/33/11433/18
parent
c6a07c124f
commit
844b0c4a2d
@ -1,13 +1,13 @@
|
|||||||
Ext.define('NgcpCsc.store.ChatList', {
|
Ext.define('NgcpCsc.store.Contacts', {
|
||||||
extend: 'Ext.data.TreeStore',
|
extend: 'Ext.data.TreeStore',
|
||||||
|
|
||||||
alias: 'store.chatlist',
|
alias: 'store.contacts',
|
||||||
|
|
||||||
storeId: 'ChatList',
|
storeId: 'Contacts',
|
||||||
|
|
||||||
proxy: {
|
proxy: {
|
||||||
type: 'ajax',
|
type: 'ajax',
|
||||||
url: 'resources/data/chatlist.json'
|
url: 'resources/data/contacts.json'
|
||||||
},
|
},
|
||||||
|
|
||||||
sorters: [{
|
sorters: [{
|
@ -0,0 +1,51 @@
|
|||||||
|
// Custom rules
|
||||||
|
$border-radius : 50%;
|
||||||
|
.rtc-panel{
|
||||||
|
.x-tool-expand-right:before{
|
||||||
|
content:'\f0d9' !important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rtc-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rtc-avatar-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rtc-avatar {
|
||||||
|
border-radius: $border-radius ;
|
||||||
|
width: 100px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
display: block;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rtc-icons {
|
||||||
|
border-radius: $border-radius ;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rtc-icons-big {
|
||||||
|
border-radius: $border-radius ;
|
||||||
|
.x-fa:before{
|
||||||
|
margin-left: -7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rtc-btns-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rtc-chat-tbar {
|
||||||
|
.x-tab-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rtc-digit {
|
||||||
|
.x-btn-inner-default-small{
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,34 +0,0 @@
|
|||||||
// Custom rules
|
|
||||||
|
|
||||||
.webrtc-container {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.webrtc-avatar-container {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.webrtc-avatar {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 100px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
display: block;
|
|
||||||
position: relative !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.webrtc-icons {
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.webrtc-btns-container {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.webrtc-chat-tbar {
|
|
||||||
.x-tab-bar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,129 @@
|
|||||||
|
.contacts {
|
||||||
|
.x-tool-expand-right:before{
|
||||||
|
content:'\f0d9' !important
|
||||||
|
}
|
||||||
|
.x-tool-collapse-left:before{
|
||||||
|
content:'\f0da' !important
|
||||||
|
}
|
||||||
|
.x-grid-row:hover {
|
||||||
|
.x-phone-display {
|
||||||
|
padding-left: 5px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
font-family: FontAwesome;
|
||||||
|
content: "\f095";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-video-display {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
font-family: FontAwesome;
|
||||||
|
content: "\f03d";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-drop-display {
|
||||||
|
position: relative;
|
||||||
|
right: 15px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
font-family: FontAwesome;
|
||||||
|
content: "\f1f8";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-add-user-display {
|
||||||
|
position: relative;
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
font-family: FontAwesome;
|
||||||
|
content: "\f234";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @include box-shadow(0px,2px,8px,0px,rgba(0,0,0,.15));
|
||||||
|
@include box-shadow(0, 1px, 2px, 0, rgba(0,0,0,0.2));
|
||||||
|
|
||||||
|
&.x-menu-default {
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-menu-header {
|
||||||
|
line-height: 20px;
|
||||||
|
background-color: $lightest-color;
|
||||||
|
padding: 22px 15px;
|
||||||
|
border-bottom: 1px solid #ccc !important;
|
||||||
|
|
||||||
|
.x-title-icon-wrap {
|
||||||
|
width: 40px;
|
||||||
|
padding-right: 28px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-title-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-menu-item-icon-default {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-left: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-menu-item {
|
||||||
|
line-height: 50px;
|
||||||
|
|
||||||
|
.x-menu-item-text-default.x-menu-item-indent-no-separator {
|
||||||
|
margin-left: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.online-user {
|
||||||
|
.x-menu-item-text-default.x-menu-item-indent-no-separator {
|
||||||
|
margin-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-menu-item-link:after {
|
||||||
|
color: $online-menu-item-color;
|
||||||
|
content: "\f111";
|
||||||
|
@extend .menu-item-common;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.x-menu-item-active {
|
||||||
|
.x-menu-item-link:after {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offline-user {
|
||||||
|
.x-menu-item-link:after {
|
||||||
|
color: $offline-menu-item-color;
|
||||||
|
content: "\f111";
|
||||||
|
@extend .menu-item-common;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-menu-item-text-default.x-menu-item-indent-no-separator {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-menu-item-link:after {
|
||||||
|
@extend .menu-item-common;
|
||||||
|
color: $default-menu-item-color;
|
||||||
|
content: "\f105";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-action-col-icon {
|
||||||
|
color: #919191;
|
||||||
|
font-size: 18px;
|
||||||
|
height: 16px;
|
||||||
|
width: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,265 @@
|
|||||||
|
Ext.define('NgcpCsc.view.common.rtc.RtcController', {
|
||||||
|
extend: 'Ext.app.ViewController',
|
||||||
|
alias: 'controller.rtc',
|
||||||
|
listen: {
|
||||||
|
controller: {
|
||||||
|
'*': {
|
||||||
|
initrtc: 'showRtcPanel',
|
||||||
|
emulateCall: 'emulateCall',
|
||||||
|
endcall: 'endCall'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
currentStream: null,
|
||||||
|
intervalId: '',
|
||||||
|
|
||||||
|
showRtcPanel: function(record, action, switchVideoOn) {
|
||||||
|
var panel = this.getView();
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
vm.set('numberToCall', '');
|
||||||
|
switch (action) {
|
||||||
|
case 'startCall':
|
||||||
|
case 'startVideoCall':
|
||||||
|
var buddyUser = Ext.getStore('Chat').findRecord('uid', record.get('uid'));
|
||||||
|
var number = (buddyUser) ? buddyUser.get('number') : record.get('caller') || record.get('source_cli') || record.get('mobile');
|
||||||
|
var mainView = Ext.ComponentQuery.query('[name=mainView]')[0];
|
||||||
|
vm.set('title', Ext.String.format('Call with {0}', number));
|
||||||
|
vm.set('thumbnail', (buddyUser) ? buddyUser.get('thumbnail') : this.getViewModel().get('defaultThumbnail'));
|
||||||
|
vm.set('status', Ext.String.format('calling {0} ...', (buddyUser) ? buddyUser.get('name') : ''));
|
||||||
|
vm.set('callEnabled', false);
|
||||||
|
vm.set('micEnabled', false);
|
||||||
|
vm.set('phoneComposerHidden', true);
|
||||||
|
vm.set('faxComposerHidden', true);
|
||||||
|
vm.set('smsComposerHidden', true);
|
||||||
|
vm.set('callPanelHidden', false);
|
||||||
|
vm.set('videoEnabled', switchVideoOn || false);
|
||||||
|
mainView.getViewModel().set('sectionTitle', 'Conversation with ' + number);
|
||||||
|
this.redirectTo('conversation-with');
|
||||||
|
this.emulateCall(true, action == 'startVideoCall');
|
||||||
|
break;
|
||||||
|
case 'phoneComposer':
|
||||||
|
if(vm.get('connected')){
|
||||||
|
this.fireEvent('showmessage', false, Ngcp.csc.locales.rtc.call_in_progress[localStorage.getItem('languageSelected')]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vm.set('title', Ngcp.csc.locales.conversations.btns.new_call[localStorage.getItem('languageSelected')]);
|
||||||
|
vm.set('phoneComposerHidden', false);
|
||||||
|
vm.set('faxComposerHidden', true);
|
||||||
|
vm.set('smsComposerHidden', true);
|
||||||
|
vm.set('callPanelHidden', true);
|
||||||
|
break;
|
||||||
|
case 'faxComposer':
|
||||||
|
if (record) {
|
||||||
|
vm.set('numberToCall', record.get('source_cli'));
|
||||||
|
}
|
||||||
|
vm.set('title', Ngcp.csc.locales.conversations.btns.new_fax[localStorage.getItem('languageSelected')]);
|
||||||
|
vm.set('phoneComposerHidden', true);
|
||||||
|
vm.set('faxComposerHidden', false);
|
||||||
|
vm.set('smsComposerHidden', true);
|
||||||
|
vm.set('callPanelHidden', true);
|
||||||
|
break;
|
||||||
|
case 'smsComposer':
|
||||||
|
if (record) {
|
||||||
|
vm.set('numberToCall', record.get('source_cli'));
|
||||||
|
}
|
||||||
|
vm.set('title', Ngcp.csc.locales.conversations.btns.new_sms[localStorage.getItem('languageSelected')]);
|
||||||
|
vm.set('phoneComposerHidden', true);
|
||||||
|
vm.set('faxComposerHidden', true);
|
||||||
|
vm.set('smsComposerHidden', false);
|
||||||
|
vm.set('callPanelHidden', true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
panel.show().expand();
|
||||||
|
},
|
||||||
|
|
||||||
|
toogleChat: function(btn) {
|
||||||
|
this.fireEvent('togglechat', btn.pressed);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleFullscreen: function() {
|
||||||
|
var video = document.querySelector("video");
|
||||||
|
var videoInProgress = false;
|
||||||
|
Ext.each(this.currentStream.getTracks(), function(mediaTrack) {
|
||||||
|
if (mediaTrack.readyState == 'live' && mediaTrack.kind == "video") {
|
||||||
|
videoInProgress = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (videoInProgress) {
|
||||||
|
if (Ext.isWebKit) {
|
||||||
|
video.webkitEnterFullScreen();
|
||||||
|
} else {
|
||||||
|
video.mozRequestFullScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minimizeRtcPanel: function() {
|
||||||
|
this.getView().collapse();
|
||||||
|
},
|
||||||
|
|
||||||
|
onBeforeClose: function() {
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
this.getView().hide();
|
||||||
|
vm.set('status', '');
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.endCall();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
emulateCall: function(audioOn, videoOn) {
|
||||||
|
var me = this;
|
||||||
|
var vm = me.getViewModel();
|
||||||
|
var sample = document.getElementById("ring");
|
||||||
|
var ringDuration = 3000;
|
||||||
|
if (this.intervalId !== '') {
|
||||||
|
clearInterval(me.intervalId);
|
||||||
|
}
|
||||||
|
vm.set('status', 'calling...');
|
||||||
|
sample.play();
|
||||||
|
setTimeout(function() {
|
||||||
|
var seconds = minutes = hours = 0;
|
||||||
|
sample.pause();
|
||||||
|
sample.currentTime = 0;
|
||||||
|
vm.set('callEnabled', true);
|
||||||
|
vm.set('micEnabled', true);
|
||||||
|
|
||||||
|
me.startMedia(audioOn, videoOn);
|
||||||
|
|
||||||
|
me.intervalId = setInterval(function() {
|
||||||
|
seconds++;
|
||||||
|
if (seconds == 60) {
|
||||||
|
seconds = 0;
|
||||||
|
minutes++;
|
||||||
|
}
|
||||||
|
if (minutes == 60) {
|
||||||
|
minutes = 0;
|
||||||
|
hours++;
|
||||||
|
}
|
||||||
|
if (hours == 24) {
|
||||||
|
hours = 0;
|
||||||
|
}
|
||||||
|
var duration = ((hours < 10) ? '0' + hours : hours) + ':' +
|
||||||
|
((minutes < 10) ? '0' + minutes : minutes) + ':' +
|
||||||
|
((seconds < 10) ? '0' + seconds : seconds);
|
||||||
|
vm.set('status', 'connected ' + duration);
|
||||||
|
}, 1000);
|
||||||
|
}, ringDuration);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAudioVideo: function() {
|
||||||
|
var me = this;
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
var video = document.querySelector("video");
|
||||||
|
Ext.each(this.currentStream.getTracks(), function(mediaTrack) {
|
||||||
|
if (mediaTrack.readyState == 'live') {
|
||||||
|
video.pause();
|
||||||
|
video.src = "";
|
||||||
|
mediaTrack.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (vm.get('micEnabled') || vm.get('videoEnabled')) {
|
||||||
|
me.startMedia(vm.get('micEnabled'), vm.get('videoEnabled'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleCall: function(btn) {
|
||||||
|
if (btn.pressed) { // this can be also checked against vm.get('callEnabled')
|
||||||
|
btn.removeCls('fa-rotate-180');
|
||||||
|
this.emulateCall(true, false);
|
||||||
|
} else {
|
||||||
|
btn.addCls('fa-rotate-180');
|
||||||
|
this.endCall();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
endCall: function() {
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
var videoObj = this.lookupReference('videoObj');
|
||||||
|
var video = document.querySelector("video");
|
||||||
|
var me = this;
|
||||||
|
this.lookupReference('avatar').show();
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
video.pause();
|
||||||
|
video.src = "";
|
||||||
|
videoObj.hide();
|
||||||
|
if(this.currentStream){
|
||||||
|
Ext.each(this.currentStream.getTracks(), function(mediaTrack) {
|
||||||
|
mediaTrack.stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
vm.set('status', 'Call ended.');
|
||||||
|
vm.set('connected', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
startMedia: function(audio, video) {
|
||||||
|
var me = this;
|
||||||
|
var vm = me.getViewModel();
|
||||||
|
//Wrap the getUserMedia function from the different browsers
|
||||||
|
navigator.getUserMedia = navigator.getUserMedia ||
|
||||||
|
navigator.webkitGetUserMedia ||
|
||||||
|
navigator.mozGetUserMedia;
|
||||||
|
|
||||||
|
//Our success callback where we get the media stream object and assign it to a video tag on the page
|
||||||
|
function onSuccess(mediaObj) {
|
||||||
|
me.currentStream = mediaObj;
|
||||||
|
me.lookupReference('avatar').setVisible(!vm.get('videoEnabled'));
|
||||||
|
me.lookupReference('videoObj').setVisible(vm.get('videoEnabled'));
|
||||||
|
vm.set('connected', true);
|
||||||
|
window.stream = mediaObj;
|
||||||
|
var video = document.querySelector("video");
|
||||||
|
video.src = window.URL.createObjectURL(mediaObj);
|
||||||
|
video.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Our error callback where we will handle any issues
|
||||||
|
function onError(errorObj) {
|
||||||
|
console.log("There was an error: " + errorObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
//We can select to request audio and video or just one of them
|
||||||
|
var mediaConstraints = {
|
||||||
|
video: video,
|
||||||
|
audio: audio
|
||||||
|
};
|
||||||
|
|
||||||
|
//Call our method to request the media object - this will trigger the browser to prompt a request.
|
||||||
|
navigator.getUserMedia(mediaConstraints, onSuccess, onError);
|
||||||
|
},
|
||||||
|
|
||||||
|
showPhoneComposer: function(btn) {
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
vm.set('phoneKeyboardHidden', !btn.pressed);
|
||||||
|
},
|
||||||
|
digitNumber: function(btn) {
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
var currentNum = vm.get('numberToCall');
|
||||||
|
vm.set('numberToCall', currentNum + btn.getText())
|
||||||
|
},
|
||||||
|
startNewCall: function() {
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
var currentNum = vm.get('numberToCall');
|
||||||
|
var record = Ext.create('NgcpCsc.model.Conversation', {
|
||||||
|
caller: currentNum
|
||||||
|
});
|
||||||
|
this.showRtcPanel(record, 'startCall');
|
||||||
|
},
|
||||||
|
sendFax: function() {
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
var mainView = Ext.ComponentQuery.query('[name=mainView]')[0];
|
||||||
|
var faxForm = this.getView().down('fax-composer');
|
||||||
|
if(faxForm.isValid()){
|
||||||
|
mainView.getViewModel().set('sectionTitle', 'Conversation with ' + vm.get('numberToCall'));
|
||||||
|
this.redirectTo('conversation-with');
|
||||||
|
faxForm.reset();
|
||||||
|
this.fireEvent('showmessage', true, Ngcp.csc.locales.rtc.fax_sent[localStorage.getItem('languageSelected')]);
|
||||||
|
}else{
|
||||||
|
this.fireEvent('showmessage', false, Ngcp.csc.locales.common.invalid_form[localStorage.getItem('languageSelected')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
sendSms: function() {
|
||||||
|
var vm = this.getViewModel();
|
||||||
|
var mainView = Ext.ComponentQuery.query('[name=mainView]')[0];
|
||||||
|
mainView.getViewModel().set('sectionTitle', 'Conversation with ' + vm.get('numberToCall'));
|
||||||
|
this.redirectTo('conversation-with');
|
||||||
|
this.fireEvent('showmessage', true, Ngcp.csc.locales.rtc.sms_sent[localStorage.getItem('languageSelected')]);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
Ext.define('NgcpCsc.view.rtc.RtcModel', {
|
||||||
|
extend: 'Ext.app.ViewModel',
|
||||||
|
|
||||||
|
alias: 'viewmodel.rtc',
|
||||||
|
|
||||||
|
data: {
|
||||||
|
title: 123456789,
|
||||||
|
defaultThumbnail: 'resources/images/icons/phoneicon.png',
|
||||||
|
thumbnail: 'resources/images/icons/phoneicon.png',
|
||||||
|
status: 'calling...',
|
||||||
|
callPanelHidden: false,
|
||||||
|
phoneComposerHidden: false,
|
||||||
|
faxComposerHidden: false,
|
||||||
|
smsComposerHidden: false,
|
||||||
|
phoneKeyboardHidden: true,
|
||||||
|
callEnabled: false,
|
||||||
|
connected: false,
|
||||||
|
micEnabled: false,
|
||||||
|
videoEnabled: false,
|
||||||
|
numberToCall: '',
|
||||||
|
// fax only fields
|
||||||
|
faxPageHeader: '',
|
||||||
|
faxContent: '',
|
||||||
|
faxSelectedQuality: '',
|
||||||
|
faxChosenFile: '',
|
||||||
|
// sms only fields
|
||||||
|
smsText: '',
|
||||||
|
// panel status
|
||||||
|
minimized: false
|
||||||
|
},
|
||||||
|
formulas: {
|
||||||
|
disableSubmit: function(get) {
|
||||||
|
var digitNumber = get('numberToCall');
|
||||||
|
if (digitNumber.length < 1) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return !digitNumber.match(/^[0-9#*+]+$/);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
disableSmsSubmit: function(get) {
|
||||||
|
return get('smsText').length > 140 || get('smsText').length < 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,137 @@
|
|||||||
|
Ext.define('NgcpCsc.view.common.rtc.RtcPanel', {
|
||||||
|
extend: 'Ext.panel.Panel',
|
||||||
|
|
||||||
|
xtype: 'rtc',
|
||||||
|
|
||||||
|
controller: 'rtc',
|
||||||
|
|
||||||
|
viewModel: 'rtc',
|
||||||
|
|
||||||
|
padding: '0 0 0 1',
|
||||||
|
|
||||||
|
width: '30%',
|
||||||
|
|
||||||
|
closable: true,
|
||||||
|
|
||||||
|
collapseDirection:'left',
|
||||||
|
|
||||||
|
cls:'rtc-panel',
|
||||||
|
|
||||||
|
bind: {
|
||||||
|
title: '{title}'
|
||||||
|
},
|
||||||
|
|
||||||
|
tools: [ {
|
||||||
|
glyph: 'xf065@FontAwesome',
|
||||||
|
callback: 'toggleFullscreen',
|
||||||
|
bind:{
|
||||||
|
hidden:'{!videoEnabled}'
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
type: 'minimize',
|
||||||
|
callback: 'minimizeRtcPanel'
|
||||||
|
}],
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
type: 'vbox',
|
||||||
|
align: 'stretch'
|
||||||
|
},
|
||||||
|
|
||||||
|
listeners: {
|
||||||
|
beforeclose: 'onBeforeClose'
|
||||||
|
},
|
||||||
|
|
||||||
|
items: [{
|
||||||
|
flex: 4,
|
||||||
|
reference: 'callpanel',
|
||||||
|
bind: {
|
||||||
|
hidden: '{callPanelHidden}'
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'vbox',
|
||||||
|
align: 'stretch',
|
||||||
|
pack: 'center'
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
cls: 'rtc-container'
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
margin:20,
|
||||||
|
hidden:true,
|
||||||
|
width:'100%',
|
||||||
|
reference:'videoObj',
|
||||||
|
html:"<video width=100% id=videoTag height=240></video>"
|
||||||
|
},{
|
||||||
|
cls: 'rtc-avatar-container',
|
||||||
|
reference:'avatar',
|
||||||
|
items: {
|
||||||
|
xtype: 'image',
|
||||||
|
cls: 'rtc-avatar',
|
||||||
|
bind: {
|
||||||
|
src: '{thumbnail}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
xtype: 'label',
|
||||||
|
margin: '20 0 20 0',
|
||||||
|
bind: '{status}'
|
||||||
|
}, {
|
||||||
|
xtype: 'container',
|
||||||
|
cls: 'rtc-btns-container',
|
||||||
|
layout: {
|
||||||
|
type: 'hbox',
|
||||||
|
align: 'stretch',
|
||||||
|
pack: 'center'
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
xtype: 'button',
|
||||||
|
cls: 'rtc-icons',
|
||||||
|
enableToggle: true
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
iconCls: 'x-fa fa-phone',
|
||||||
|
bind: {
|
||||||
|
pressed: '{callEnabled}'
|
||||||
|
},
|
||||||
|
handler:'toggleCall'
|
||||||
|
}, {
|
||||||
|
iconCls: 'x-fa fa-microphone',
|
||||||
|
bind: {
|
||||||
|
pressed: '{micEnabled}',
|
||||||
|
disabled: '{!connected}',
|
||||||
|
hidden: '{!micEnabled}'
|
||||||
|
},
|
||||||
|
handler:'toggleAudioVideo'
|
||||||
|
},{
|
||||||
|
iconCls: 'x-fa fa-microphone-slash',
|
||||||
|
bind: {
|
||||||
|
pressed: '{micEnabled}',
|
||||||
|
disabled: '{!connected}',
|
||||||
|
hidden: '{micEnabled}'
|
||||||
|
},
|
||||||
|
handler:'toggleAudioVideo'
|
||||||
|
}, {
|
||||||
|
iconCls: 'x-fa fa-video-camera',
|
||||||
|
bind: {
|
||||||
|
pressed: '{videoEnabled}',
|
||||||
|
disabled: '{!connected}'
|
||||||
|
},
|
||||||
|
handler:'toggleAudioVideo'
|
||||||
|
}, {
|
||||||
|
iconCls: 'x-fa fa-comment',
|
||||||
|
bind: {
|
||||||
|
pressed: '{chatEnabled}',
|
||||||
|
disabled: '{!connected}'
|
||||||
|
},
|
||||||
|
handler: 'toogleChat'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
xtype: 'phone-composer'
|
||||||
|
}, {
|
||||||
|
xtype: 'sms-composer'
|
||||||
|
}, {
|
||||||
|
xtype: 'fax-composer'
|
||||||
|
}]
|
||||||
|
})
|
@ -0,0 +1,75 @@
|
|||||||
|
Ext.define('NgcpCsc.view.common.composer.Fax', {
|
||||||
|
extend: 'Ext.form.Panel',
|
||||||
|
|
||||||
|
alias: 'widget.fax-composer',
|
||||||
|
|
||||||
|
bind: {
|
||||||
|
hidden: '{faxComposerHidden}'
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
width: '90%',
|
||||||
|
margin: 20
|
||||||
|
},
|
||||||
|
|
||||||
|
items: [{
|
||||||
|
layout: 'hbox',
|
||||||
|
items: [{
|
||||||
|
xtype: 'textfield',
|
||||||
|
emptyText: 'Allowed digits are 0-9, +, # and *.',
|
||||||
|
hideTrigger: true,
|
||||||
|
width: '80%',
|
||||||
|
bind: '{numberToCall}'
|
||||||
|
}, {
|
||||||
|
xtype: 'button',
|
||||||
|
enableToggle: true,
|
||||||
|
iconCls: 'x-fa fa-fax',
|
||||||
|
width: '20%',
|
||||||
|
handler: 'showPhoneComposer'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
xtype: 'phonekeys',
|
||||||
|
bind: {
|
||||||
|
hidden: '{phoneKeyboardHidden}'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
xtype: 'combo',
|
||||||
|
store: ['Normal', 'Fine', 'Super'],
|
||||||
|
fieldLabel: 'Quality',
|
||||||
|
allowBlank: false,
|
||||||
|
editable: false,
|
||||||
|
bind: '{faxSelectedQuality}'
|
||||||
|
}, {
|
||||||
|
xtype: 'textfield',
|
||||||
|
allowBlank: false,
|
||||||
|
bind: '{faxSageHeader}',
|
||||||
|
fieldLabel: 'Page header'
|
||||||
|
}, {
|
||||||
|
xtype: 'textarea',
|
||||||
|
allowBlank: false,
|
||||||
|
bind: '{faxContent}',
|
||||||
|
fieldLabel: 'Content'
|
||||||
|
}, {
|
||||||
|
xtype: 'filefield',
|
||||||
|
bind: '{faxContenthosenFile}',
|
||||||
|
fieldLabel: 'File'
|
||||||
|
}, {
|
||||||
|
xtype: 'container',
|
||||||
|
layout: 'center',
|
||||||
|
margin: '40 20 20 20',
|
||||||
|
items: [{
|
||||||
|
xtype: 'button',
|
||||||
|
cls: 'rtc-icons-big',
|
||||||
|
bind: {
|
||||||
|
disabled: '{disableSubmit}'
|
||||||
|
},
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
margin: '50 0 10 0',
|
||||||
|
iconCls: 'x-fa fa-send fa-2x',
|
||||||
|
cls: 'rtc-icons-big',
|
||||||
|
listeners: {
|
||||||
|
click: 'sendFax'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
})
|
@ -0,0 +1,56 @@
|
|||||||
|
Ext.define('NgcpCsc.view.common.composer.Phone', {
|
||||||
|
extend: 'Ext.panel.Panel',
|
||||||
|
|
||||||
|
alias: 'widget.phone-composer',
|
||||||
|
|
||||||
|
bind: {
|
||||||
|
hidden: '{phoneComposerHidden}'
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
layout: 'hbox',
|
||||||
|
margin: 20,
|
||||||
|
items: [{
|
||||||
|
xtype: 'textfield',
|
||||||
|
emptyText: 'Allowed digits are 0-9, +, # and *.',
|
||||||
|
hideTrigger: true,
|
||||||
|
width: '80%',
|
||||||
|
bind: '{numberToCall}'
|
||||||
|
}, {
|
||||||
|
xtype: 'button',
|
||||||
|
enableToggle: true,
|
||||||
|
iconCls: 'x-fa fa-fax',
|
||||||
|
width: '20%',
|
||||||
|
handler: 'showPhoneComposer'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
xtype: 'phonekeys',
|
||||||
|
bind: {
|
||||||
|
hidden: '{phoneKeyboardHidden}'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
xtype: 'container',
|
||||||
|
layout: 'center',
|
||||||
|
margin: '40 20 20 20',
|
||||||
|
items: [{
|
||||||
|
xtype: 'button',
|
||||||
|
cls: 'rtc-icons-big',
|
||||||
|
bind: {
|
||||||
|
disabled: '{disableSubmit}'
|
||||||
|
},
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
margin: '50 0 10 0',
|
||||||
|
iconCls: 'x-fa fa-phone fa-3x',
|
||||||
|
cls: 'rtc-icons-big',
|
||||||
|
listeners: {
|
||||||
|
click: {
|
||||||
|
fn: 'startNewCall',
|
||||||
|
el: 'element'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
hidden: true,
|
||||||
|
html: '<audio id="ring" src="resources/audio/skype_ring.mp3" preload="auto"></audio>'
|
||||||
|
}]
|
||||||
|
})
|
@ -0,0 +1,66 @@
|
|||||||
|
Ext.define('NgcpCsc.view.common.composer.PhoneKeys', {
|
||||||
|
extend: 'Ext.panel.Panel',
|
||||||
|
|
||||||
|
alias: 'widget.phonekeys',
|
||||||
|
|
||||||
|
hidden: true,
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
type: 'vbox',
|
||||||
|
align: 'stretch'
|
||||||
|
},
|
||||||
|
|
||||||
|
height: 300,
|
||||||
|
|
||||||
|
margin: '30 10 10 20',
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
flex: 1,
|
||||||
|
layout: {
|
||||||
|
type: 'hbox',
|
||||||
|
align: 'stretch',
|
||||||
|
pack:'center'
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
xtype: 'button',
|
||||||
|
flex: 1,
|
||||||
|
margin: '0 10 10 0',
|
||||||
|
handler: 'digitNumber',
|
||||||
|
cls: 'rtc-digit',
|
||||||
|
maxWidth:90
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
items: [{
|
||||||
|
text: '1'
|
||||||
|
}, {
|
||||||
|
text: '2'
|
||||||
|
}, {
|
||||||
|
text: '3'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
items: [{
|
||||||
|
text: '4'
|
||||||
|
}, {
|
||||||
|
text: '5'
|
||||||
|
}, {
|
||||||
|
text: '6'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
items: [{
|
||||||
|
text: '7'
|
||||||
|
}, {
|
||||||
|
text: '8'
|
||||||
|
}, {
|
||||||
|
text: '9'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
items: [{
|
||||||
|
text: '*'
|
||||||
|
}, {
|
||||||
|
text: '0'
|
||||||
|
}, {
|
||||||
|
text: '#'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
})
|
@ -0,0 +1,62 @@
|
|||||||
|
Ext.define('NgcpCsc.view.common.composer.Sms', {
|
||||||
|
extend: 'Ext.panel.Panel',
|
||||||
|
|
||||||
|
alias: 'widget.sms-composer',
|
||||||
|
|
||||||
|
bind: {
|
||||||
|
hidden: '{smsComposerHidden}'
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
width: '90%',
|
||||||
|
margin: 20
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
layout: 'hbox',
|
||||||
|
items: [{
|
||||||
|
xtype: 'textfield',
|
||||||
|
emptyText: 'Allowed digits are 0-9, +, # and *.',
|
||||||
|
hideTrigger: true,
|
||||||
|
width: '80%',
|
||||||
|
bind: '{numberToCall}'
|
||||||
|
}, {
|
||||||
|
xtype: 'button',
|
||||||
|
enableToggle: true,
|
||||||
|
iconCls: 'x-fa fa-fax',
|
||||||
|
width: '20%',
|
||||||
|
handler: 'showPhoneComposer'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
xtype: 'phonekeys',
|
||||||
|
bind: {
|
||||||
|
hidden: '{phoneKeyboardHidden}'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
xtype: 'textarea',
|
||||||
|
allowBlank: false,
|
||||||
|
bind: '{smsText}',
|
||||||
|
fieldLabel: 'Content',
|
||||||
|
emptyText: 'Max 140 digits.'
|
||||||
|
}, {
|
||||||
|
xtype: 'container',
|
||||||
|
layout: 'center',
|
||||||
|
margin: '40 20 20 20',
|
||||||
|
items: [{
|
||||||
|
xtype: 'button',
|
||||||
|
cls: 'rtc-icons-big',
|
||||||
|
bind: {
|
||||||
|
disabled: '{disableSmsSubmit}'
|
||||||
|
},
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
margin: '50 0 10 0',
|
||||||
|
iconCls: 'x-fa fa-send fa-2x',
|
||||||
|
cls: 'rtc-icons-big',
|
||||||
|
listeners: {
|
||||||
|
click: {
|
||||||
|
fn: 'sendSms',
|
||||||
|
el: 'element'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
})
|
@ -1,74 +0,0 @@
|
|||||||
Ext.define('NgcpCsc.view.common.webrtc.WebrtcController', {
|
|
||||||
extend: 'Ext.app.ViewController',
|
|
||||||
alias: 'controller.webrtc',
|
|
||||||
|
|
||||||
listen: {
|
|
||||||
controller: {
|
|
||||||
'*': {
|
|
||||||
initwebrtc: 'showWebrtcPanel',
|
|
||||||
startcall: 'startCall',
|
|
||||||
pausecall: 'pauseCall',
|
|
||||||
endcall: 'endCall',
|
|
||||||
startvideocall: 'startVideoCall',
|
|
||||||
pausevideocall: 'pauseVideoCall',
|
|
||||||
endvideocall: 'endVideoCall'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showWebrtcPanel: function(record, switchVideoOn, newCall) {
|
|
||||||
var panel = this.getView();
|
|
||||||
if(!newCall){
|
|
||||||
var vm = this.getViewModel();
|
|
||||||
var buddyUser = Ext.getStore('Chat').findRecord('uid', record.get('uid'));
|
|
||||||
var number = (buddyUser) ? buddyUser.get('number') : record.get('caller') || record.get('source_cli') || record.get('mobile');
|
|
||||||
this.getViewModel().set('title', Ext.String.format('{0}', number));
|
|
||||||
vm.set('thumbnail', (buddyUser) ? buddyUser.get('thumbnail') : this.getViewModel().get('defaultThumbnail'));
|
|
||||||
vm.set('status', Ext.String.format('calling {0} ...', (buddyUser) ? buddyUser.get('name') : ''));
|
|
||||||
vm.set('callEnabled', true);
|
|
||||||
vm.set('micEnabled', true);
|
|
||||||
vm.set('videoEnabled', switchVideoOn || false);
|
|
||||||
}
|
|
||||||
panel.show().expand();
|
|
||||||
},
|
|
||||||
|
|
||||||
toogleChat: function(btn) {
|
|
||||||
this.fireEvent('togglechat', btn.pressed);
|
|
||||||
},
|
|
||||||
|
|
||||||
onBeforeClose: function(){
|
|
||||||
this.getView().hide();
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
startCall: function() {
|
|
||||||
//TODO
|
|
||||||
},
|
|
||||||
|
|
||||||
pauseCall: function() {
|
|
||||||
//TODO
|
|
||||||
},
|
|
||||||
|
|
||||||
endCall: function() {
|
|
||||||
//TODO
|
|
||||||
},
|
|
||||||
|
|
||||||
startVideoCall: function() {
|
|
||||||
//TODO
|
|
||||||
},
|
|
||||||
|
|
||||||
pauseVideoCall: function() {
|
|
||||||
//TODO
|
|
||||||
},
|
|
||||||
|
|
||||||
endVideoCall: function() {
|
|
||||||
//TODO
|
|
||||||
},
|
|
||||||
|
|
||||||
onPressSubmitBtn: function() {
|
|
||||||
// TODO
|
|
||||||
},
|
|
||||||
onPressEnter: function() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,15 +0,0 @@
|
|||||||
Ext.define('NgcpCsc.view.webrtc.WebrtcModel', {
|
|
||||||
extend: 'Ext.app.ViewModel',
|
|
||||||
|
|
||||||
alias: 'viewmodel.webrtc',
|
|
||||||
|
|
||||||
data: {
|
|
||||||
title: 123456789,
|
|
||||||
defaultThumbnail: 'resources/images/icons/phoneicon.png',
|
|
||||||
thumbnail: 'resources/images/icons/phoneicon.png',
|
|
||||||
status: 'calling...',
|
|
||||||
callEnabled: false,
|
|
||||||
micEnabled: false,
|
|
||||||
videoEnabled: false
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,108 +0,0 @@
|
|||||||
Ext.define('NgcpCsc.view.common.webrtc.WebrtcPanel', {
|
|
||||||
extend: 'Ext.panel.Panel',
|
|
||||||
|
|
||||||
xtype: 'webrtc',
|
|
||||||
|
|
||||||
controller: 'webrtc',
|
|
||||||
|
|
||||||
viewModel: 'webrtc',
|
|
||||||
|
|
||||||
padding: '0 0 0 1',
|
|
||||||
|
|
||||||
width: '30%',
|
|
||||||
|
|
||||||
closable: true,
|
|
||||||
|
|
||||||
bind: {
|
|
||||||
title: '{title}'
|
|
||||||
},
|
|
||||||
|
|
||||||
layout: {
|
|
||||||
type: 'vbox',
|
|
||||||
align: 'stretch'
|
|
||||||
},
|
|
||||||
|
|
||||||
listeners:{
|
|
||||||
beforeclose: 'onBeforeClose'
|
|
||||||
},
|
|
||||||
|
|
||||||
initComponent: function() {
|
|
||||||
this.items = [{
|
|
||||||
flex: 4,
|
|
||||||
layout: {
|
|
||||||
type: 'vbox',
|
|
||||||
align: 'stretch',
|
|
||||||
pack: 'center'
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
cls: 'webrtc-container'
|
|
||||||
},
|
|
||||||
items: [{
|
|
||||||
cls: 'webrtc-avatar-container',
|
|
||||||
items: {
|
|
||||||
xtype: 'image',
|
|
||||||
cls: 'webrtc-avatar',
|
|
||||||
bind: {
|
|
||||||
src: '{thumbnail}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
xtype: 'label',
|
|
||||||
margin: '20 0 20 0',
|
|
||||||
bind: '{status}'
|
|
||||||
}, {
|
|
||||||
xtype: 'container',
|
|
||||||
cls: 'webrtc-btns-container',
|
|
||||||
layout: {
|
|
||||||
type: 'hbox',
|
|
||||||
align: 'stretch',
|
|
||||||
pack: 'center'
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
xtype: 'button',
|
|
||||||
cls: 'webrtc-icons',
|
|
||||||
enableToggle: true
|
|
||||||
},
|
|
||||||
items: [{
|
|
||||||
iconCls: 'x-fa fa-phone',
|
|
||||||
bind: {
|
|
||||||
pressed: '{callEnabled}'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
iconCls: 'x-fa fa-microphone',
|
|
||||||
bind: {
|
|
||||||
pressed: '{micEnabled}'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
iconCls: 'x-fa fa-video-camera',
|
|
||||||
bind: {
|
|
||||||
pressed: '{videoEnabled}'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
iconCls: 'x-fa fa-wechat',
|
|
||||||
bind: {
|
|
||||||
pressed: '{chatEnabled}'
|
|
||||||
},
|
|
||||||
handler: 'toogleChat'
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}, Ext.create('NgcpCsc.view.pages.chat.ChatContainer', {
|
|
||||||
flex: 3,
|
|
||||||
cls: 'webrtc-chat-tbar',
|
|
||||||
bind: {
|
|
||||||
hidden: '{!chatEnabled}'
|
|
||||||
},
|
|
||||||
items: [{
|
|
||||||
xtype: 'chat-notifications',
|
|
||||||
closable: false,
|
|
||||||
scrollable: true,
|
|
||||||
cls: 'private-conversation-text',
|
|
||||||
deferEmptyText: false,
|
|
||||||
store: Ext.create('Ext.data.Store', {
|
|
||||||
model: 'NgcpCsc.model.ChatNotification'
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
})];
|
|
||||||
this.callParent();
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,11 +0,0 @@
|
|||||||
Ext.define('NgcpCsc.view.pages.chat.ChatListModel', {
|
|
||||||
extend: 'Ext.app.ViewModel',
|
|
||||||
|
|
||||||
alias: 'viewmodel.chatlist',
|
|
||||||
|
|
||||||
stores: {
|
|
||||||
buddyList: {
|
|
||||||
type: 'chatlist'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -0,0 +1,11 @@
|
|||||||
|
Ext.define('NgcpCsc.view.pages.contacts.ContactsModel', {
|
||||||
|
extend: 'Ext.app.ViewModel',
|
||||||
|
|
||||||
|
alias: 'viewmodel.contacts',
|
||||||
|
|
||||||
|
stores: {
|
||||||
|
buddyList: {
|
||||||
|
type: 'contacts'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Binary file not shown.
Loading…
Reference in new issue