1 | window.onbeforeunload = function (e) {
|
2 | var dialogText = 'Please, close the appointment by pressing "End call" or the appointment will stay active!';
|
3 | e.returnValue = dialogText;
|
4 | return dialogText;
|
5 | };
|
6 |
|
7 | function getCookie(name) {
|
8 | var nameEQ = name + "=";
|
9 | var ca = document.cookie.split(';');
|
10 | for(var i=0;i < ca.length;i++) {
|
11 | var c = ca[i];
|
12 | while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
13 | if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
|
14 | }
|
15 | return null;
|
16 | }
|
17 |
|
18 | $(document).ready(function () {
|
19 | $(window).on('beforeunload', function () {
|
20 | return 'Please, close the appointment by pressing "End call" or the appointment will stay active!';
|
21 | });
|
22 | var appointmentId = window.location.search.split('?appointmentId=').pop();
|
23 | var ROLE_DOCTOR = 'IDCR';
|
24 | var socket = io.connect();
|
25 | var user;
|
26 |
|
27 | var PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
28 | var IceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
|
29 | var SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
|
30 | navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
|
31 | var pc;
|
32 | var notificationServiceWorker = null;
|
33 | var localStream = null;
|
34 | var timer;
|
35 | var notifications = [];
|
36 | var restartCallTimer = null;
|
37 | var constraints = {
|
38 | audio: true,
|
39 | video: true
|
40 | };
|
41 | var token = getCookie('JSESSIONID');
|
42 |
|
43 | getNotificationPermission();
|
44 |
|
45 | if (navigator.mediaDevices.getUserMedia) {
|
46 | navigator.mediaDevices.getUserMedia(constraints).then(setLocalStream).catch(errorHandler);
|
47 | } else {
|
48 | navigator.getUserMedia(constraints).then(setLocalStream).catch(errorHandler);
|
49 | }
|
50 |
|
51 | |
52 |
|
53 |
|
54 | function cretePeerConnection() {
|
55 | gotStream(localStream);
|
56 | }
|
57 |
|
58 | function setLocalStream(stream) {
|
59 |
|
60 | $.get('/api/user').then(function (usr) {
|
61 | user = usr;
|
62 | if (!user.username) {
|
63 | user.username = user.email.split('@')[0];
|
64 | }
|
65 | socket.emit('user:init', {
|
66 | username: user.username,
|
67 | nhsNumber: user.nhsNumber,
|
68 | role: user.role,
|
69 | surname: user.family_name,
|
70 | name: user.given_name,
|
71 | token: token
|
72 | });
|
73 | })
|
74 | .fail(function(error) {
|
75 | console.log('error! ' + JSON.stringify(error.responseJSON));
|
76 | if (error.responseJSON.error && error.responseJSON.error) {
|
77 | console.log('You are not logged in or your session has expired');
|
78 | return;
|
79 | }
|
80 | });
|
81 |
|
82 |
|
83 | localStream = stream;
|
84 | cretePeerConnection();
|
85 | }
|
86 |
|
87 | function gotStream(stream) {
|
88 | $('#localVideo').toggleClass('inactive').attr('src', URL.createObjectURL(stream));
|
89 |
|
90 | pc = new PeerConnection({
|
91 | "iceServers": [{url: 'stun:stun01.sipphone.com'},
|
92 | {url: 'stun:stun.ekiga.net'},
|
93 | {url: 'stun:stun.fwdnet.net'},
|
94 | {url: 'stun:stun.ideasip.com'},
|
95 | {url: 'stun:stun.iptel.org'},
|
96 | {url: 'stun:stun.rixtelecom.se'},
|
97 | {url: 'stun:stun.schlund.de'},
|
98 | {url: 'stun:stun.l.google.com:19302'},
|
99 | {url: 'stun:stun1.l.google.com:19302'},
|
100 | {url: 'stun:stun2.l.google.com:19302'},
|
101 | {url: 'stun:stun3.l.google.com:19302'},
|
102 | {url: 'stun:stun4.l.google.com:19302'},
|
103 | {url: 'stun:stunserver.org'},
|
104 | {url: 'stun:stun.softjoys.com'},
|
105 | {url: 'stun:stun.voiparound.com'},
|
106 | {url: 'stun:stun.voipbuster.com'},
|
107 | {url: 'stun:stun.voipstunt.com'},
|
108 | {url: 'stun:stun.voxgratia.org'},
|
109 | {url: 'stun:stun.xten.com'},
|
110 | {
|
111 | url: 'turn:numb.viagenie.ca',
|
112 | credential: 'muazkh',
|
113 | username: 'webrtc@live.com'
|
114 | },
|
115 | {
|
116 | url: 'turn:192.158.29.39:3478?transport=udp',
|
117 | credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
|
118 | username: '28224511:1379330808'
|
119 | },
|
120 | {
|
121 | url: 'turn:192.158.29.39:3478?transport=tcp',
|
122 | credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
|
123 | username: '28224511:1379330808'
|
124 | }]
|
125 | });
|
126 |
|
127 | pc.addStream(stream);
|
128 | pc.onicecandidate = gotIceCandidate;
|
129 | pc.onaddstream = gotRemoteStream;
|
130 |
|
131 |
|
132 | if ($('#muteAudio').hasClass('inactive')) {
|
133 | toggleAudioStreams(false);
|
134 | }
|
135 | if ($('#muteVideo').hasClass('inactive')) {
|
136 | toggleVideoStreams(false);
|
137 | toggleVideo('#localVideo');
|
138 | }
|
139 | }
|
140 |
|
141 | function createOffer() {
|
142 | var options = {
|
143 | 'mandatory': {
|
144 | 'OfferToReceiveAudio': true,
|
145 | 'OfferToReceiveVideo': true
|
146 | }
|
147 | };
|
148 | pc.createOffer(options)
|
149 | .then(gotLocalDescription)
|
150 | .catch(errorHandler);
|
151 | }
|
152 |
|
153 | function createAnswer() {
|
154 | var options = {
|
155 | 'mandatory': {
|
156 | 'OfferToReceiveAudio': true,
|
157 | 'OfferToReceiveVideo': true
|
158 | }
|
159 | };
|
160 | pc.createAnswer(options)
|
161 | .then(gotLocalDescription)
|
162 | .catch(errorHandler);
|
163 | }
|
164 |
|
165 |
|
166 | function gotLocalDescription(description) {
|
167 | pc.setLocalDescription(description);
|
168 | sendWebRTCMessage(description);
|
169 | }
|
170 |
|
171 | function gotIceCandidate(event) {
|
172 | if (event.candidate) {
|
173 | sendWebRTCMessage({
|
174 | type: 'candidate',
|
175 | label: event.candidate.sdpMLineIndex,
|
176 | id: event.candidate.sdpMid,
|
177 | candidate: event.candidate.candidate
|
178 | });
|
179 | }
|
180 | }
|
181 |
|
182 | function gotRemoteStream(event) {
|
183 | $('#remoteVideo').removeClass('inactive').attr('src', URL.createObjectURL(event.stream));
|
184 | $('#noSound').hide();
|
185 | socket.emit('call:remoteStreamProp:get', {appointmentId: appointmentId, token: token});
|
186 | }
|
187 |
|
188 | function errorHandler(err) {
|
189 | console.error(err);
|
190 | }
|
191 |
|
192 | |
193 |
|
194 |
|
195 |
|
196 | function sendWebRTCMessage(message) {
|
197 | socket.emit('call:webrtc:message', {message: message, appointmentId: appointmentId, token: token});
|
198 | }
|
199 |
|
200 | socket.on('user:init', function(data) {
|
201 | console.log('user:init response - ' + JSON.stringify(data));
|
202 | if (data.ok) {
|
203 | socket.emit('call:init', {
|
204 | appointmentId: appointmentId,
|
205 | token: token
|
206 | });
|
207 | }
|
208 | });
|
209 |
|
210 | socket.on('call:webrtc:init', function (data) {
|
211 | $('#remoteVideo').removeClass('inactive');
|
212 | if (data.isInitiator) {
|
213 | createOffer();
|
214 | }
|
215 | });
|
216 |
|
217 | socket.on('call:getPatientInfo', function (data) {
|
218 | $.get('/api/patients/' + data.patientId).then(function (data) {
|
219 | $('#patientName').text(data.name);
|
220 | $('#patientDob').text(moment(data.dateOfBirth).format('DD-MMM-YYYY'));
|
221 | $('#patientGender').text(data.gender);
|
222 | $('#patientNhs').text(data.nhsNumber);
|
223 | });
|
224 | $.get('/api/patients/' + data.patientId + '/appointments/' + appointmentId).then(function (data) {
|
225 | var appointment = data;
|
226 | if (!appointment) {
|
227 | appointment = {
|
228 | author: "c4h_ripple_osi",
|
229 | dateCreated: 1446216927376,
|
230 | dateOfAppointment: 1423612800000,
|
231 | location: "Leeds Royal Infirmary",
|
232 | serviceTeam: "Prostate cancer MDT Team",
|
233 | source: "Marand",
|
234 | sourceId: "88536cbb-2f09-4624-a8da-fd468f045e60::ripple_osi.ehrscape.c4h::3",
|
235 | status: "Scheduled",
|
236 | timeOfAppointment: 50400000,
|
237 | }
|
238 | }
|
239 | $('#appointmentCst').text(appointment.serviceTeam);
|
240 | $('#appointmentLocation').text(appointment.location);
|
241 | });
|
242 | });
|
243 |
|
244 | socket.on('call:opponent:join', function (data) {
|
245 | addTextMessage(data.timestamp, null, data.message + ' has entered the chat room');
|
246 | createNotification({title: data.message + ' has entered the chat room'});
|
247 | });
|
248 |
|
249 | socket.on('call:busy', function () {
|
250 | if (pc.signalingState !== 'closed') {
|
251 | pc.close();
|
252 | }
|
253 | toggleVideo('#remoteVideo');
|
254 | window.close();
|
255 | });
|
256 |
|
257 | socket.on('call:opponent:left', function (data) {
|
258 | addTextMessage(data.timestamp, null, data.message + ' has left the chat room');
|
259 | if ($(window).data('isBlur')) {
|
260 | createNotification({
|
261 | title: data.message + ' has left the chat room'
|
262 | });
|
263 | }
|
264 | if (pc.signalingState !== 'closed') {
|
265 | pc.close();
|
266 | }
|
267 | $('#remoteVideo').attr('src', '').addClass('inactive');
|
268 | $('#noSound').hide();
|
269 | cretePeerConnection();
|
270 | });
|
271 |
|
272 | socket.on('call:webrtc:message', function (message) {
|
273 | if (message.type === 'offer') {
|
274 | pc.setRemoteDescription(new SessionDescription(message));
|
275 | createAnswer();
|
276 | } else if (message.type === 'answer') {
|
277 | pc.setRemoteDescription(new SessionDescription(message));
|
278 | } else if (message.type === 'candidate') {
|
279 | var candidate = new IceCandidate({sdpMLineIndex: message.label, candidate: message.candidate});
|
280 | pc.addIceCandidate(candidate);
|
281 | }
|
282 | });
|
283 |
|
284 | socket.on('call:text:message', function (data) {
|
285 | addTextMessage(data.timestamp, data.author, data.message);
|
286 | if ($(window).data('isBlur')) {
|
287 | createNotification({
|
288 | title: 'You received a new Message' + (data.author ? (' from ' + data.author) : ''),
|
289 | body: data.message
|
290 | });
|
291 | }
|
292 | });
|
293 |
|
294 | socket.on('call:text:messages:history', function (data) {
|
295 | var role = isDoctor(user) ? 'doctor' : 'patient';
|
296 | var opponent = data.appointment[(isDoctor(user) ? 'patient' : 'doctor')];
|
297 | for (var i = 0; i < data.messages.length; i++) {
|
298 | addTextMessage(data.messages[i].timestamp, (data.messages[i].author) ? ((role == data.messages[i].author) ? 'You' : opponent) : null, data.messages[i].message, true);
|
299 | }
|
300 | });
|
301 |
|
302 | socket.on('call:timer', function (data) {
|
303 | console.log(data);
|
304 | clearInterval(timer);
|
305 | initTimer(data.timestamp);
|
306 | });
|
307 |
|
308 | socket.on('call:remoteStreamProp:get', function () {
|
309 | var remoteStreamProp = {};
|
310 | remoteStreamProp.audio = localStream.getAudioTracks()[0].enabled;
|
311 | remoteStreamProp.video = localStream.getVideoTracks()[0].enabled;
|
312 | socket.emit('call:remoteStreamProp:post', {appointmentId: appointmentId, remoteStreamProp: remoteStreamProp, token: token})
|
313 | });
|
314 |
|
315 | socket.on('call:remoteStreamProp:post', function (data) {
|
316 | if (!data.remoteStreamProp.audio) {
|
317 | toggleAudio();
|
318 | }
|
319 | if (!data.remoteStreamProp.video) {
|
320 | toggleVideo('#remoteVideo');
|
321 | }
|
322 | });
|
323 |
|
324 | socket.on('call:video:toggle', function () {
|
325 | toggleVideo('#remoteVideo');
|
326 | });
|
327 |
|
328 | socket.on('call:audio:toggle', function () {
|
329 | toggleAudio();
|
330 | });
|
331 |
|
332 | socket.on('call:restart', function (data) {
|
333 | clearInterval(timer);
|
334 | addTextMessage(data.timestamp, null, data.user + ' has restarted the appointment');
|
335 | createNotification({title: data.user + ' has restarted the appointment'});
|
336 |
|
337 | clearTimeout(restartCallTimer);
|
338 | restartCallTimer = null;
|
339 | $('#sendMessage button').removeClass('disabled');
|
340 |
|
341 | $('#endCall').show();
|
342 | $('#closeWindow').hide();
|
343 | $('#restartCall').hide();
|
344 | var duration = getDiffTime(Date.now(), data.created_at);
|
345 | $('#callDuration').appendTo('.video-chat-controller').empty().append('Connected - <span id="timer">' + duration + '</span>');
|
346 | clearInterval(timer);
|
347 | initTimer(data.created_at);
|
348 | $('#localVideo').css('display', '');
|
349 | cretePeerConnection();
|
350 |
|
351 | });
|
352 |
|
353 | socket.on('call:close', function (data) {
|
354 | if (pc.signalingState !== 'closed') {
|
355 | pc.close();
|
356 | }
|
357 | if (!data) return;
|
358 | if (data.user) {
|
359 | addTextMessage(data.timestamp, null, data.user + ' has ended the conversation');
|
360 | createNotification({title: data.user + ' has ended the conversation'});
|
361 | }
|
362 |
|
363 | $('#remoteVideo').css('background', '').attr('src', '').addClass('inactive');
|
364 | $('#localVideo').attr('src', '').hide();
|
365 |
|
366 | var callDuration = getDiffTime(Date.now(), data.created_at);
|
367 | $('#endCall').hide();
|
368 | $('#closeWindow').show();
|
369 | $('#restartCall').removeClass('disabled').show();
|
370 |
|
371 | var endedIn = moment.utc(data.timestamp).add(60, 's').valueOf();
|
372 |
|
373 | console.log('moment 1', moment.utc(endedIn).diff(Date.now(), 'seconds'), moment.utc(endedIn).format('HH:mm'), moment.utc(Date.now()).format('HH:mm'));
|
374 |
|
375 | if (moment.utc(endedIn).diff(Date.now(), 'seconds') > 60) {
|
376 | console.log('moment rly', moment.utc(endedIn).diff(Date.now(), 'seconds'), moment.utc(endedIn).format('HH:mm'), moment.utc(Date.now()).format('HH:mm'));
|
377 | endedIn = moment.utc(Date.now()).add(59, 's').valueOf();
|
378 | }
|
379 |
|
380 | restartCallTimer = setTimeout(function () {
|
381 | $('#restartCall').addClass('disabled');
|
382 | $('#sendMessage button').addClass('disabled');
|
383 | clearInterval(timer);
|
384 | $('#callDuration').appendTo('.video-wrapper').empty().append('Call Ended<br/>Duration - <span id="duration">' + callDuration + '</span>');
|
385 | }, moment.utc(endedIn).diff(Date.now(), 'milliseconds'));
|
386 |
|
387 | $('#callDuration').appendTo('.video-wrapper').empty().append('Call Ended in: <span id="timer">01:00</span><br/>Duration - <span id="duration">' + callDuration + '</span>');
|
388 |
|
389 | clearInterval(timer);
|
390 | initTimer(endedIn, true);
|
391 | });
|
392 |
|
393 | |
394 |
|
395 |
|
396 |
|
397 | $('#muteAudio').off('click').on('click', function () {
|
398 | $(this).toggleClass('inactive').toggleClass('fa-microphone').toggleClass('fa-microphone-slash');
|
399 | toggleAudioStreams();
|
400 | socket.emit('call:audio:toggle', {appointmentId: appointmentId, token: token});
|
401 | });
|
402 |
|
403 | $('#muteVideo').off('click').on('click', function () {
|
404 | $(this).toggleClass('inactive').toggleClass('fa-video-camera-2').toggleClass('fa-video-camera-slash-2');
|
405 | toggleVideoStreams();
|
406 | toggleVideo('#localVideo');
|
407 | socket.emit('call:video:toggle', {appointmentId: appointmentId, token: token});
|
408 | });
|
409 |
|
410 | $('#endCall').off('click').on('click', function () {
|
411 | $('#endCallModal').fadeIn();
|
412 | });
|
413 |
|
414 | $('#closeWindow').off('click').on('click', function () {
|
415 | window.close();
|
416 | });
|
417 |
|
418 | $('#restartCall').off('click').on('click', function () {
|
419 | clearTimeout(restartCallTimer);
|
420 | restartCallTimer = null;
|
421 |
|
422 | socket.emit('call:restart', {appointmentId: appointmentId, token: token});
|
423 | });
|
424 |
|
425 | $('#endCallModal-cancel').off('click').on('click', function () {
|
426 | $('#endCallModal').fadeOut();
|
427 | });
|
428 |
|
429 | $('#endCallModal-submit').off('click').on('click', function () {
|
430 | $('#endCallModal').fadeOut();
|
431 | socket.emit('call:close', {appointmentId: appointmentId, token: token});
|
432 | });
|
433 |
|
434 | $('#sendMessage').off('keydown').on('keydown', function (e) {
|
435 | if (e.ctrlKey && e.keyCode == 13) {
|
436 | $(this).submit();
|
437 | }
|
438 | }).on('submit', function (e) {
|
439 | e.preventDefault();
|
440 | var message = $(this).find('textarea').val();
|
441 | message = message.replace(/<(.|\n)*?>/g, '');
|
442 | $(this).find('textarea').val('');
|
443 | if (message == false) return;
|
444 |
|
445 | socket.emit('call:text:message', {appointmentId: appointmentId, message: message, token: token})
|
446 | });
|
447 |
|
448 | $(window).on("blur focus", function (e) {
|
449 | var prevType = $(this).data("isBlur") ? 'blur' : 'focus';
|
450 |
|
451 | if (prevType != e.type) {
|
452 | switch (e.type) {
|
453 | case 'blur':
|
454 | break;
|
455 | case 'focus':
|
456 | while (notifications.length) {
|
457 | var notification = notifications.pop();
|
458 | notification.close();
|
459 | }
|
460 | break;
|
461 | }
|
462 | }
|
463 |
|
464 | $(this).data('isBlur', e.type == 'blur');
|
465 | });
|
466 |
|
467 | function toggleVideo(video_selector) {
|
468 | var $video = $(video_selector);
|
469 | $video.toggleClass('inactive');
|
470 | if (video_selector.indexOf('remote') !== -1) return;
|
471 | var src = $video.attr('src');
|
472 | if (!src) {
|
473 | src = $video.data('src');
|
474 | $video.attr('src', src);
|
475 | } else {
|
476 | $video.data('src', src);
|
477 | $video.attr('src', '');
|
478 | }
|
479 | }
|
480 |
|
481 | function toggleAudio() {
|
482 | $('#noSound').toggle();
|
483 | }
|
484 |
|
485 | function toggleAudioStreams(enabled) {
|
486 | localStream.getAudioTracks().forEach(function (stream) {
|
487 | stream.enabled = (enabled !== undefined) ? enabled : !stream.enabled;
|
488 | });
|
489 | }
|
490 |
|
491 | function toggleVideoStreams(enabled) {
|
492 | localStream.getVideoTracks().forEach(function (stream) {
|
493 | stream.enabled = (enabled !== undefined) ? enabled : !stream.enabled;
|
494 | });
|
495 | }
|
496 |
|
497 | function addTextMessage(timestamp, author, message, prepend) {
|
498 | var $list = $('.list-messages');
|
499 | var li = document.createElement('li');
|
500 | var textNode = document.createTextNode(moment.utc(timestamp).local().format('HH:mm') + ' - ' + ( (author !== null) ? (author + ': ') : '') + message);
|
501 | $(li).append(textNode);
|
502 | $list[(prepend) ? 'prepend' : 'append'](li);
|
503 | var height = $list[0].scrollHeight;
|
504 | $list.scrollTop(height);
|
505 | }
|
506 |
|
507 | function createNotification(data) {
|
508 | if (!('Notification' in window)) {
|
509 | console.log('This browser does not support desktop notification');
|
510 | } else if (Notification.permission === 'granted') {
|
511 | _createNotification(data);
|
512 | } else if (Notification.permission !== 'denied') {
|
513 | Notification.requestPermission(function (permission) {
|
514 | if (permission === 'granted') {
|
515 | _createNotification(data)
|
516 | }
|
517 | });
|
518 | }
|
519 | }
|
520 |
|
521 | function _createNotification(data) {
|
522 | try {
|
523 | notifications.push(new Notification(data.title, {
|
524 | body: data.body,
|
525 | icon: '../images/ripple-icon.png'
|
526 | }));
|
527 | playSound('alert');
|
528 | } catch (err) {
|
529 | if (err.name == 'TypeError') {
|
530 | if (notificationServiceWorker === null) {
|
531 | notificationServiceWorker = navigator.serviceWorker.register('/scripts/chat/sw.js');
|
532 | }
|
533 | notificationServiceWorker.then(function (registration) {
|
534 | registration.showNotification(data.title, {
|
535 | body: data.body,
|
536 | icon: '../images/ripple-icon.png',
|
537 | vibrate: [200, 100, 200, 100, 200, 100, 200],
|
538 | });
|
539 | registration.getNotifications().then(function (data) {
|
540 | notifications = [].concat(data);
|
541 | });
|
542 | });
|
543 | }
|
544 | }
|
545 | }
|
546 |
|
547 | function getNotificationPermission(cb) {
|
548 | if (!('Notification' in window)) return;
|
549 | if (Notification.permission !== 'denied' || Notification.permission !== 'granted') {
|
550 | Notification.requestPermission(cb);
|
551 | }
|
552 | }
|
553 |
|
554 | function playSound(filename) {
|
555 | $('#notificationSound').empty().html('<audio autoplay="autoplay">' +
|
556 | '<source src="sounds/' + filename + '.mp3" type="audio/mpeg" />' +
|
557 | '<source src="sounds/' + filename + '.ogg" type="audio/ogg" />' +
|
558 | '<embed hidden="true" autostart="true" loop="false" src="sounds/' + filename + '.mp3" />' +
|
559 | '</audio>');
|
560 | }
|
561 |
|
562 | function isDoctor(user) {
|
563 | return user && user.role == ROLE_DOCTOR;
|
564 | }
|
565 |
|
566 | function initTimer(time, withoutHours) {
|
567 | console.log('initTimer', time);
|
568 | timer = setInterval(function () {
|
569 | $('#timer').text(getDiffTime(Date.now(), time, withoutHours));
|
570 | }, 1000);
|
571 | }
|
572 |
|
573 | function getDiffTime(from, to, withoutHours) {
|
574 | return ((withoutHours) ?
|
575 | [
|
576 | ('0' + (Math.abs(moment.utc(from).diff(to, 'minutes')) % 60)).slice(-2),
|
577 | ('0' + (Math.abs(moment.utc(from).diff(to, 'seconds')) % 60)).slice(-2)] :
|
578 | [
|
579 | ('0' + Math.abs(moment.utc(from).diff(to, 'hours'))).slice(-2),
|
580 | ('0' + (Math.abs(moment.utc(from).diff(to, 'minutes')) % 60)).slice(-2),
|
581 | ('0' + (Math.abs(moment.utc(from).diff(to, 'seconds')) % 60)).slice(-2)
|
582 | ]).join(':');
|
583 | }
|
584 | }); |
\ | No newline at end of file |