Minor addition to message center help file (#1745)
[openemr.git] / portal / messaging / secure_chat.php
blobb09ebfad2c0b268722fc5c85086883650d4a8dd3
1 <?php
2 /**
4 * Copyright (C) 2016-2017 Jerry Padgett <sjpadgett@gmail.com>
6 * LICENSE: This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, either version 3 of the
9 * License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
16 * You should have received a copy of the GNU Affero General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 * @package OpenEMR
20 * @author Jerry Padgett <sjpadgett@gmail.com>
21 * @link http://www.open-emr.org
24 namespace SMA_Common;
26 session_start();
27 if (isset($_SESSION['pid']) && isset($_SESSION['patient_portal_onsite_two'])) {
28 $pid = $_SESSION['pid'];
29 $ignoreAuth = true;
30 require_once(dirname(__FILE__) . "/../../interface/globals.php");
31 define('IS_DASHBOARD', false);
32 define('IS_PORTAL', $_SESSION['pid']);
33 } else {
34 session_destroy();
35 $ignoreAuth = false;
36 require_once(dirname(__FILE__) . "/../../interface/globals.php");
37 if (! isset($_SESSION['authUserID'])) {
38 $landingpage = "index.php";
39 header('Location: '.$landingpage);
40 exit;
43 define('IS_DASHBOARD', $_SESSION['authUserID']);
44 define('IS_PORTAL', false);
45 $_SERVER[REMOTE_ADDR] = 'admin::' . $_SERVER[REMOTE_ADDR];
48 define('C_USER', IS_PORTAL ? IS_PORTAL : IS_DASHBOARD);
50 if (isset($_REQUEST['fullscreen'])) {
51 $_SESSION['whereto'] = 'messagespanel';
52 define('IS_FULLSCREEN', true);
53 } else {
54 define('IS_FULLSCREEN', false);
57 define('CHAT_HISTORY', '150');
58 define('CHAT_ONLINE_RANGE', '1');
59 define('ADMIN_USERNAME_PREFIX', 'adm_');
61 abstract class Model
63 public $db;
65 public function __construct()
67 //$this->db = new \mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME);
71 abstract class Controller
73 private $_request, $_response, $_query, $_post, $_server, $_cookies, $_session;
74 protected $_currentAction, $_defaultModel;
76 const ACTION_POSTFIX = 'Action';
77 const ACTION_DEFAULT = 'indexAction';
79 public function __construct()
81 $this->_request = &$_REQUEST;
82 $this->_query = &$_GET;
83 $this->_post = &$_POST;
84 $this->_server = &$_SERVER;
85 $this->_cookies = &$_COOKIE;
86 $this->_session = &$_SESSION;
87 $this->init();
90 public function init()
92 $this->dispatchActions();
93 $this->render();
96 public function dispatchActions()
98 $action = $this->getQuery('action');
99 if ($action && $action .= self::ACTION_POSTFIX) {
100 if (method_exists($this, $action)) {
101 $this->setResponse(
102 call_user_func(array($this, $action), array())
104 } else {
105 $this->setHeader("HTTP/1.0 404 Not Found");
107 } else {
108 $this->setResponse(
109 call_user_func(array($this, self::ACTION_DEFAULT), array())
113 return $this->_response;
116 public function render()
118 if ($this->_response) {
119 if (is_scalar($this->_response)) {
120 echo $this->_response;
121 } else {
122 throw new \Exception('Response content must be scalar');
125 exit;
129 public function indexAction()
131 return null;
134 public function setResponse($content)
136 $this->_response = $content;
139 public function setHeader($params)
141 if (! headers_sent()) {
142 if (is_scalar($params)) {
143 header($params);
144 } else {
145 foreach ($params as $key => $value) {
146 header(sprintf('%s: %s', $key, $value));
151 return $this;
154 public function setModel($namespace)
156 $this->_defaultModel = $namespace;
157 return $this;
160 public function setSession($key, $value)
162 $_SESSION[$key] = $value;
163 return $this;
166 public function setCookie($key, $value, $seconds = 3600)
168 $this->_cookies[$key] = $value;
169 if (! headers_sent()) {
170 setcookie($key, $value, time() + $seconds);
171 return $this;
175 public function getRequest($param = null, $default = null)
177 if ($param) {
178 return isset($this->_request[$param]) ?
179 $this->_request[$param] : $default;
182 return $this->_request;
185 public function getQuery($param = null, $default = null)
187 if ($param) {
188 return isset($this->_query[$param]) ?
189 $this->_query[$param] : $default;
192 return $this->_query;
195 public function getPost($param = null, $default = null)
197 if ($param) {
198 return isset($this->_post[$param]) ?
199 $this->_post[$param] : $default;
202 return $this->_post;
205 public function getServer($param = null, $default = null)
207 if ($param) {
208 return isset($this->_server[$param]) ?
209 $this->_server[$param] : $default;
212 return $this->_server;
215 public function getSession($param = null, $default = null)
217 if ($param) {
218 return isset($this->_session[$param]) ?
219 $this->_session[$param] : $default;
222 return $this->_session;
225 public function getCookie($param = null, $default = null)
227 if ($param) {
228 return isset($this->_cookies[$param]) ?
229 $this->_cookies[$param] : $default;
232 return $this->_cookies;
235 public function getUser()
237 return $this->_session['ptName'] ? $this->_session['ptName'] : $this->_session['authUser'];
239 public function getIsPortal()
241 return IS_PORTAL;
243 public function getIsFullScreen()
245 return IS_FULLSCREEN;
247 public function getModel()
249 if ($this->_defaultModel && class_exists($this->_defaultModel)) {
250 return new $this->_defaultModel;
254 public function sanitize($string, $quotes = ENT_QUOTES, $charset = 'utf-8')
256 return htmlentities($string, $quotes, $charset);
260 abstract class Helper
265 namespace SMA_Msg;
267 // @codingStandardsIgnoreStart
268 use SMA_Common;
269 // @codingStandardsIgnoreEnd
270 class Model extends SMA_Common\Model
272 public function getAuthUsers()
274 $response = sqlStatementNoLog("SELECT patient_data.pid as recip_id, Concat_Ws(' ', patient_data.fname, patient_data.lname) as username FROM patient_data WHERE allow_patient_portal = 'YES'");
275 $resultpd = array ();
276 while ($row = sqlFetchArray($response)) {
277 $resultpd[] = $row;
280 $response = sqlStatementNoLog("SELECT users.id as recip_id, users.authorized as dash, CONCAT(users.fname,' ',users.lname) as username FROM users WHERE authorized = 1");
281 $result = array ();
282 while ($row = sqlFetchArray($response)) {
283 $result[] = $row;
286 return json_encode(array_merge($result, $resultpd));
288 public function getMessages($limit = CHAT_HISTORY, $reverse = true)
290 $response = sqlStatementNoLog("(SELECT * FROM onsite_messages
291 ORDER BY `date` DESC LIMIT {$limit}) ORDER BY `date` ASC");
293 $result = array();
294 while ($row = sqlFetchArray($response)) {
295 if (IS_PORTAL) {
296 $u = json_decode($row['recip_id'], true);
297 if (!is_array($u)) {
298 continue;
301 if ((in_array(C_USER, $u)) || $row['sender_id'] == C_USER) {
302 $result[] = $row; // only current patient messages
304 } else {
305 $result[] = $row; // admin gets all
309 return $result;
312 public function addMessage($username, $message, $ip, $senderid = 0, $recipid = '')
314 return sqlQueryNoLog("INSERT INTO onsite_messages VALUES (NULL, ?, ?, ?, NOW(), ?, ?)", array($username,$message,$ip,$senderid,$recipid));
317 public function removeMessages()
319 return sqlQueryNoLog("TRUNCATE TABLE onsite_messages");
322 public function removeOldMessages($limit = CHAT_HISTORY)
324 /* @todo Patched out to replace with soft delete. Besides this query won't work with current ado(or any) */
325 /* return sqlStatementNoLog("DELETE FROM onsite_messages
326 WHERE id NOT IN (SELECT id FROM onsite_messages
327 ORDER BY date DESC LIMIT {$limit})"); */
330 public function getOnline($count = true, $timeRange = CHAT_ONLINE_RANGE)
332 if ($count) {
333 $response = sqlStatementNoLog("SELECT count(*) as total FROM onsite_online");
334 return sqlFetchArray($response);
337 $response = sqlStatementNoLog("SELECT * FROM onsite_online");
338 $result = array();
339 while ($row = sqlFetchArray($response)) {
340 $result[] = $row;
343 return $result;
346 public function updateOnline($hash, $ip, $username = '', $userid = 0)
348 return sqlStatementNoLog("REPLACE INTO onsite_online
349 VALUES ( ?, ?, NOW(), ?, ? )", array($hash, $ip, $username, $userid)) or die(mysql_error());
352 public function clearOffline($timeRange = CHAT_ONLINE_RANGE)
354 return sqlStatementNoLog("DELETE FROM onsite_online
355 WHERE last_update <= (NOW() - INTERVAL {$timeRange} MINUTE)");
358 public function __destruct()
363 class Controller extends SMA_Common\Controller
365 protected $_model;
367 public function __construct()
369 $this->setModel('SMA_Msg\Model');
370 parent::__construct();
373 public function indexAction()
376 public function authusersAction()
378 return $this->getModel()->getAuthUsers(true);
380 public function listAction()
382 $this->setHeader(array('Content-Type' => 'application/json'));
383 $messages = $this->getModel()->getMessages();
384 foreach ($messages as &$message) {
385 $message['me'] = C_USER === $message['sender_id']; // $this->getServer('REMOTE_ADDR') === $message['ip'];
388 return json_encode($messages);
391 public function saveAction()
393 $username = $this->getPost('username');
394 $message = $this->getPost('message');
395 $ip = $this->getServer('REMOTE_ADDR');
396 $this->setCookie('username', $username, 9999 * 9999);
397 $recipid = $this->getPost('recip_id');
399 if (IS_PORTAL) {
400 $senderid = IS_PORTAL;
401 } else {
402 $senderid = IS_DASHBOARD;
405 $result = array('success' => false);
406 if ($username && $message) {
407 $cleanUsername = preg_replace('/^'.ADMIN_USERNAME_PREFIX.'/', '', $username);
408 $result = array(
409 'success' => $this->getModel()->addMessage($cleanUsername, $message, $ip, $senderid, $recipid)
413 if ($this->_isAdmin($username)) {
414 $this->_parseAdminCommand($message);
417 $this->setHeader(array('Content-Type' => 'application/json'));
418 return json_encode($result);
421 private function _isAdmin($username)
423 return IS_DASHBOARD?true:false;
424 //return preg_match('/^'.ADMIN_USERNAME_PREFIX.'/', $username);
427 private function _parseAdminCommand($message)
429 if (strpos($message, '/clear') !== false) {
430 $this->getModel()->removeMessages();
431 return true;
434 if (strpos($message, '/online') !== false) {
435 $online = $this->getModel()->getOnline(false);
436 $ipArr = array();
437 foreach ($online as $item) {
438 $ipArr[] = $item->ip;
441 $message = 'Online: ' . implode(", ", $ipArr);
442 $this->getModel()->addMessage('Admin Command', $message, '0.0.0.0');
443 return true;
447 private function _getMyUniqueHash()
449 $unique = $this->getServer('REMOTE_ADDR');
450 $unique .= $this->getServer('HTTP_USER_AGENT');
451 $unique .= $this->getServer('HTTP_ACCEPT_LANGUAGE');
452 $unique .= C_USER;
453 return md5($unique);
456 public function pingAction()
458 $ip = $this->getServer('REMOTE_ADDR');
459 $hash = $this->_getMyUniqueHash();
460 $user = $this->getRequest('username', 'No Username');
461 if ($user == 'currentol') {
462 $onlines = $this->getModel()->getOnline(false);
463 $this->setHeader(array('Content-Type' => 'application/json'));
464 return json_encode($onlines);
467 if (IS_PORTAL) {
468 $userid = IS_PORTAL;
469 } else {
470 $userid = IS_DASHBOARD;
473 $this->getModel()->updateOnline($hash, $ip, $user, $userid);
474 $this->getModel()->clearOffline();
475 // $this->getModel()->removeOldMessages(); // @todo For soft delete when I decide. DO NOT REMOVE
477 $onlines = $this->getModel()->getOnline();
479 $this->setHeader(array('Content-Type' => 'application/json'));
480 return json_encode($onlines);
484 $msgApp = new Controller();
486 <!doctype html>
487 <html ng-app="MsgApp">
488 <head>
489 <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
490 <meta charset="utf-8">
492 <title><?php echo xlt('Secure Patient Chat'); ?></title>
493 <meta name="author" content="Jerry Padgett sjpadgett{{at}} gmail {{dot}} com">
495 <script type='text/javascript' src="<?php echo $GLOBALS['assets_static_relative']; ?>/jquery-min-1-11-3/index.js"></script>
497 <link href="<?php echo $GLOBALS['assets_static_relative']; ?>/bootstrap-3-3-4/dist/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
498 <?php if ($_SESSION['language_direction'] == 'rtl') { ?>
499 <link href="<?php echo $GLOBALS['assets_static_relative']; ?>/bootstrap-rtl-3-3-4/dist/css/bootstrap-rtl.min.css" rel="stylesheet" type="text/css" />
500 <?php } ?>
502 <script type='text/javascript' src="<?php echo $GLOBALS['assets_static_relative']; ?>/bootstrap-3-3-4/dist/js/bootstrap.min.js"></script>
504 <link rel="stylesheet" href="<?php echo $GLOBALS['assets_static_relative']; ?>/summernote-0-8-2/dist/summernote.css" />
505 <script type='text/javascript' src="<?php echo $GLOBALS['assets_static_relative']; ?>/summernote-0-8-2/dist/summernote.js"></script>
507 <script type='text/javascript' src="<?php echo $GLOBALS['assets_static_relative']; ?>/angular-1-5-8/angular.min.js"></script>
508 <script type='text/javascript' src="<?php echo $GLOBALS['assets_static_relative']; ?>/angular-summernote-0-8-1/dist/angular-summernote.js"></script>
509 <script type='text/javascript' src="<?php echo $GLOBALS['assets_static_relative']; ?>/angular-sanitize-1-5-8/angular-sanitize.min.js"></script>
510 <script src='<?php echo $GLOBALS['assets_static_relative']; ?>/checklist-model-0-10-0/checklist-model.js'></script>
512 </head>
513 <script type="text/javascript">
514 (function() {
515 var MsgApp = angular.module('MsgApp',['ngSanitize','summernote',"checklist-model"]);
516 MsgApp.config(function( $compileProvider ) {
517 $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|file|ftp|blob):|data:image\//);
520 MsgApp.directive('ngEnter', function () {
521 return function (scope, element, attrs) {
522 element.bind("keydown keypress", function (event) {
523 if (event.which === 13) {
524 scope.$apply(function (){
525 scope.$eval(attrs.ngEnter);
527 event.preventDefault();
532 MsgApp.directive('tooltip', function(e){
533 return {
534 restrict: 'A',
535 link: function(scope, element, attrs){
536 $(element).hover(function(){
537 $(element).tooltip('show');
538 }, function(){
539 $(element).tooltip('hide');
544 MsgApp.filter('unique', function() {
545 return function(collection, keyname) {
546 var output = [],
547 keys = [];
548 angular.forEach(collection, function(item) {
549 var key = item[keyname];
550 if(keys.indexOf(key) === -1) {
551 keys.push(key);
552 output.push(item);
555 return output;
559 MsgApp.controller('MsgAppCtrl', ['$scope', '$http', '$filter', function($scope, $http, $filter) {
560 $scope.urlListMessages = '?action=list'; // actions for restful
561 $scope.urlSaveMessage = '?action=save';
562 $scope.urlListOnlines = '?action=ping';
563 $scope.urlGetAuthUsers = '?action=authusers';
565 $scope.pidMessages = null;
566 $scope.pidPingServer = null;
568 $scope.beep = new Audio('beep.ogg'); // you've got mail!!!! really just a beep
569 $scope.messages = [];
570 $scope.online = null;
571 $scope.lastMessageId = null;
572 $scope.historyFromId = null;
573 $scope.onlines = []; // all online users id and ip's
574 $scope.user = "<?php echo $_SESSION['ptName'] ? $_SESSION['ptName'] : $_SESSION['authUser'];?>" // current user - dashboard user is from session authUserID
575 $scope.userid = "<?php echo IS_PORTAL ? $_SESSION['pid'] : $_SESSION['authUserID'];?>"
576 $scope.isPortal = "<?php echo IS_PORTAL;?>" ;
577 $scope.isFullScreen = "<?php echo IS_FULLSCREEN; ?>";
578 $scope.pusers = []; // selected recipients for chat
579 $scope.chatusers = []; // authorize chat recipients for dashboard user
580 $scope.me = {
581 username: $scope.user,
582 message: null,
583 sender_id: $scope.userid,
584 recip_id: 0
586 $scope.options = {
587 height: 200,
588 focus: true,
589 placeholder: 'Start typing your message...',
590 //direction: 'rtl',
591 toolbar: [
592 ['style', ['bold', 'italic', 'underline', 'clear']],
593 ['fontsize', ['fontsize']],
594 ['color', ['color']],
595 ['para', ['ul', 'ol', 'paragraph']],
596 ['insert', ['link','picture', 'video', 'hr']],
597 ['view', ['fullscreen', 'codeview']]
600 $scope.checkAll = function() {
601 $scope.pusers = [];
602 $scope.pusers = $scope.chatusers.map(function(item) { return item.recip_id; });
603 $scope.getAuthUsers();
605 $scope.uncheckAll = function() {
606 $scope.pusers = [];
607 $scope.getAuthUsers();
609 $scope.makeCurrent = function(sel) {
610 if( !sel.me ){
611 $scope.pusers.splice(0, $scope.pusers.length);
612 $scope.pusers.push(sel.sender_id);
615 $scope.pageTitleNotificator = {
616 vars: {
617 originalTitle: window.document.title,
618 interval: null,
619 status: 0
621 on: function(title, intervalSpeed) {
622 var self = this;
623 if (! self.vars.status) {
624 self.vars.interval = window.setInterval(function() {
625 window.document.title = (self.vars.originalTitle == window.document.title) ?
626 title : self.vars.originalTitle;
627 }, intervalSpeed || 500);
628 self.vars.status = 1;
631 off: function() {
632 window.clearInterval(this.vars.interval);
633 window.document.title = this.vars.originalTitle;
634 this.vars.status = 0;
638 $scope.editmsg = function() {
639 $('.summernote').summernote();
642 $scope.saveedit = function() {
643 var makrup = $('.summernote').summernote('code');
644 $scope.me.message = makrup;
645 $scope.saveMessage();
646 $('.summernote').summernote('code', ''); //add this options to reset editor or not-default is persistant content
647 //$('.summernote').summernote('destroy');
650 $scope.saveMessage = function(form, callback) {
651 $scope.me.recip_id = JSON.stringify(angular.copy($scope.pusers));
652 var data = $.param($scope.me);
653 if (! ($scope.me.username && $scope.me.username.trim())) {
654 return $scope.openModal();
656 if (! ($scope.me.message && $scope.me.message.trim() &&
657 $scope.me.username && $scope.me.username.trim())) {
658 return;
660 $scope.me.message = '';
661 return $http({
662 method: 'POST',
663 url: $scope.urlSaveMessage,
664 data: data,
665 headers: {'Content-Type': 'application/x-www-form-urlencoded'}
666 }).success(function(data) {
667 $scope.listMessages(true);
671 $scope.replaceShortcodes = function(message) {
672 var msg = '';
673 msg = message.toString().replace(/(\[img])(.*)(\[\/img])/, "<img class='img-responsive' src='$2' />");
674 msg = msg.toString().replace(/(\[url])(.*)(\[\/url])/, "<a href='$2'>$2</a>");
675 msg = message.toString().replace("<img ", "<img class='img-responsive' ");
676 return msg;
679 $scope.notifyLastMessage = function() {
680 if (typeof window.Notification === 'undefined') {
681 return;
683 window.Notification.requestPermission(function (permission) {
684 var lastMessage = $scope.getLastMessage();
685 if (permission == 'granted' && lastMessage && lastMessage.username) {
686 var notify = new window.Notification('Message notification from ' + lastMessage.username + ' : ', {
687 body: 'New message' //lastMessage.message
689 notify.onclick = function() {
690 window.focus();
692 notify.onclose = function() {
693 $scope.pageTitleNotificator.off();
695 var timmer = setInterval(function() {
696 notify && notify.close();
697 typeof timmer !== 'undefined' && window.clearInterval(timmer);
698 }, 10000);
703 $scope.getLastMessage = function() {
704 return $scope.messages[$scope.messages.length - 1];
707 $scope.listMessages = function(wasListingForMySubmission) {
708 return $http.post($scope.urlListMessages, {}).success(function(data) {
709 $scope.messages = [];
710 angular.forEach(data, function(message) {
711 message.message = $scope.replaceShortcodes(message.message);
712 $scope.messages.push(message);
715 var lastMessage = $scope.getLastMessage();
716 var lastMessageId = lastMessage && lastMessage.id;
718 if ($scope.lastMessageId !== lastMessageId) {
719 $scope.onNewMessage(wasListingForMySubmission);
721 $scope.lastMessageId = lastMessageId;
722 if($scope.pusers == ''){ // overkill but may need later
723 angular.forEach($filter('unique')($scope.messages,'sender_id'), function(m,k){
724 var flg = false;
725 angular.forEach($scope.pusers, function(id) {
726 if(id == m.sender_id){ flg = true; }
728 if(!flg) $scope.pusers.push(m.sender_id);
731 $scope.getOnlines();
735 $scope.onNewMessage = function(wasListingForMySubmission) {
736 if ($scope.lastMessageId && !wasListingForMySubmission) {
737 $scope.playAudio();
738 $scope.pageTitleNotificator.on('New message');
739 $scope.notifyLastMessage();
741 $scope.scrollDown();
742 window.addEventListener('focus', function() {
743 $scope.pageTitleNotificator.off();
746 $scope.getAuthUsers = function() {
747 $scope.chatusers = [];
748 return $http.post($scope.urlGetAuthUsers, {}).success(function(data) {
749 $scope.chatusers = data;
752 $scope.pingServer = function(msgItem) {
753 return $http.post($scope.urlListOnlines+'&username='+$scope.user, {}).success(function(data) {
754 $scope.online = data;
759 $scope.getOnlines = function() {
760 return $http.post($scope.urlListOnlines+'&username=currentol', {}).success(function(data) {
761 $scope.onlines = data;
765 $scope.init = function() {
766 $scope.listMessages();
767 $scope.pidMessages = window.setInterval($scope.listMessages, 6000);
768 $scope.pidPingServer = window.setInterval($scope.pingServer, 10000);
769 $scope.getAuthUsers();
770 $("#popeditor").on("show.bs.modal", function() {
771 var height = $(window).height() - 200;
772 $(this).find(".modal-body").css("max-height", height);
776 $scope.scrollDown = function() {
777 var pidScroll;
778 pidScroll = window.setInterval(function() {
779 $('.direct-chat-messages').scrollTop(window.Number.MAX_SAFE_INTEGER * 0.001);
780 window.clearInterval(pidScroll);
781 }, 100);
784 $scope.clearHistory = function() {
785 var lastMessage = $scope.getLastMessage();
786 var lastMessageId = lastMessage && lastMessage.id;
787 lastMessageId = (lastMessageId-1 >= 2) ? lastMessageId -1 : lastMessageId;
788 lastMessageId && ($scope.historyFromId = lastMessageId);
791 $scope.openModal = function(e) {
792 var mi = $('#popeditor').modal({backdrop: "static"});
793 //$scope.editmsg();
796 $scope.playAudio = function() {
797 $scope.beep && $scope.beep.play();
800 $scope.renderMessageBody = function(html)
802 return html;
804 $scope.init();
805 }]);
806 })();
807 </script>
808 <style>
809 .direct-chat-text {
810 border-radius:5px;
811 position:relative;
812 padding:5px 10px;
813 background:#FBFBFB;
814 border:1px solid #C2C6CE;
815 margin:5px 0 0 50px;
816 color:#444;
818 .direct-chat-msg,.direct-chat-text {
819 display:block;
820 word-wrap: break-word;
822 .direct-chat-img {
823 border-radius:50%;
824 float:left;
825 width:40px;
826 height:40px;
828 .direct-chat-info {
829 display:block;
830 margin-bottom:2px;
831 font-size:12px;
833 .direct-chat-msg {
834 margin-bottom:5px;
836 .direct-chat-messages,.direct-chat-contacts {
837 -webkit-transition:-webkit-transform .5s ease-in-out;
838 -moz-transition:-moz-transform .5s ease-in-out;
839 -o-transition:-o-transform .5s ease-in-out;
840 transition:transform .5s ease-in-out;
842 .direct-chat-messages {
843 -webkit-transform:translate(0,0);
844 -ms-transform:translate(0,0);
845 -o-transform:translate(0,0);
846 transform:translate(0,0);
847 padding: 5px;
848 height: calc(100vh - 175px);
849 /* height: 400px; */
850 /*height:100%; */
851 overflow:auto;
852 word-wrap: break-word;
854 .direct-chat-text:before {
855 border-width:6px;
856 margin-top:-6px;
858 .direct-chat-text:after {
859 border-width:5px;
860 margin-top:-5px;
862 .direct-chat-text:after,.direct-chat-text:before {
863 position:absolute;
864 right:100%;
865 top:15px;
866 border:solid rgba(0,0,0,0);
867 border-right-color:#D2D6DE;
868 content:' ';
869 height:0;
870 width:0;
871 pointer-events:none;
873 .direct-chat-warning .right>.direct-chat-text {
874 background:#fef7ec;
875 border-color:#F39C12;
876 color:#000;
878 .right .direct-chat-text {
879 margin-right:50px;
880 margin-left:0;
882 .direct-chat-warning .right>.direct-chat-text:after,
883 .direct-chat-warning .right>.direct-chat-text:before {
884 border-left-color:#F39C12;
886 .right .direct-chat-text:after,.right .direct-chat-text:before {
887 right:auto;
888 left:100%;
889 border-right-color:rgba(0,0,0,0);
890 border-left-color:#D2D6DE;
892 .right .direct-chat-img {
893 float:right;
895 .box-footer {
896 border-top-left-radius:0;
897 border-top-right-radius:0;
898 border-bottom-right-radius:3px;
899 border-bottom-left-radius:3px;
900 border-top:1px solid #F4F4F4;
901 padding:10px 0;
902 background-color:#FFF;
904 .direct-chat-name {
905 font-weight:600;
907 .box-footer form {
908 margin-bottom:10px;
910 input,button,.alert,.modal-content {
911 border-radius: 0!important;
913 .ml10 {
914 margin-left:10px;
916 .ml5 {
917 margin-left:5px;
919 .sidebar{
920 background-color: lightgrey;
921 height:100%;
922 margin-top:5px;
923 margin-right:0;
924 padding-right:5px;
925 max-height: 530px;
926 overflow: auto;
928 .rtsidebar{
929 background-color: lightgrey;
930 height:100%;
931 margin-top:5px;
932 margin-right:0;
934 .fixed-panel{
935 height: 100%;
936 padding: 5px 5px 0 5px;
938 label {display: block;}
939 legend{
940 font-size:14px;
941 margin-bottom:2px;
942 background:#fff;
944 .modal.modal-wide .modal-dialog {
945 width: 75%;
947 .modal-wide .modal-body {
948 overflow-y: auto;
950 </style>
952 <body ng-controller="MsgAppCtrl">
953 <div class="container">
954 <!-- <h2 class="hidden-xs">Secure Chat</h2> -->
955 <div class="row">
956 <div class="col-md-2 sidebar">
957 <h4><span class="label label-danger"><?php echo xlt('In Current Chat'); ?></span></h4>
958 <label ng-repeat="user in chatusers | unique : 'username'" ng-if="pusers.indexOf(user.recip_id) !== -1 && user.recip_id != me.sender_id">
959 <input type="checkbox" data-checklist-model="pusers" data-checklist-value="user.recip_id"> {{user.username}}
960 </label>
961 <h4><span class="label label-warning"><?php echo xlt('Authorized Users'); ?></span></h4>
962 <span>
963 <button id="chkall" class="btn btn-xs btn-success" ng-show="!isPortal" ng-click="checkAll()" type="button"><?php echo xlt('All'); ?></button>
964 <button id="chknone" class="btn btn-xs btn-success" ng-show="!isPortal" ng-click="uncheckAll()" type="button"><?php echo xlt('None'); ?></button>
965 </span>
966 <label ng-repeat="user in chatusers | unique : 'username'" ng-show="!isPortal || (isPortal && user.dash)">
967 <input type="checkbox" data-checklist-model="pusers" data-checklist-value="user.recip_id"> {{user.username}}
968 </label>
969 </div>
970 <div class="col-md-8 fixed-panel">
971 <div class="panel panel-warning direct-chat direct-chat-warning">
972 <div class="panel-heading">
973 <div class="clearfix">
974 <a class="btn btn-sm btn-primary pull-right ml10" href=""
975 data-toggle="modal" data-target="#clear-history"><?php echo xlt('Clear history'); ?></a>
976 <a class="btn btn-sm btn-success pull-left ml10" href="./../patient/provider" ng-show="!isPortal"><?php echo xlt('Home'); ?></a>
977 <a class="btn btn-sm btn-success pull-left ml10" href="./../home.php" ng-show="isFullScreen"><?php echo xlt('Home'); ?></a>
978 </div>
979 </div>
980 <div class="panel-body">
981 <div class="direct-chat-messages">
982 <div class="direct-chat-msg" ng-repeat="message in messages" ng-if="historyFromId < message.id" ng-class="{'right':!message.me}">
983 <div class="direct-chat-info clearfix">
984 <span class="direct-chat-name"
985 ng-class="{'pull-left':message.me, 'pull-right':!message.me}">{{message.username }}</span>
986 <span class="direct-chat-timestamp "
987 ng-class="{'pull-left':!message.me, 'pull-right':message.me}">{{message.date }}</span>
988 </div>
989 <img class="direct-chat-img" ng-show="!message.me"
990 src="./../images/Unknown-person.gif"
991 alt="">
992 <img class="direct-chat-img" ng-show="message.me")
993 src="<?php echo $GLOBALS['images_static_relative']; ?>/favicon-32x32.png"
994 alt="">
995 <div class="direct-chat-text right">
996 <div style="padding-left: 0px; padding-right: 0px;" title="<?php echo xlt('Click to make chat this current recipient only...'); ?>" ng-click="makeCurrent(message)" ng-bind-html=renderMessageBody(message.message)></div>
997 </div>
998 </div>
999 </div>
1000 <div class="panel-footer box-footer-hide">
1001 <form id='msgfrm' ng-submit="saveMessage()">
1002 <div class="input-group">
1003 <input type="text" placeholder="Type message..." id="msgedit" autofocus="autofocus" class="form-control" ng-model="me.message" ng-enter="saveMessage()">
1004 <span class="input-group-btn">
1005 <button type="submit" class="btn btn-danger btn-flat"><?php echo xlt('Send'); ?></button>
1006 <button type="button" class="btn btn-success btn-flat" ng-click="openModal(event)"><?php echo xlt('Edit'); ?></button>
1007 </span>
1008 </div>
1009 </form>
1010 </div>
1011 </div>
1012 </div>
1013 </div>
1014 <div class="col-md-2 rtsidebar">
1015 <h4><span class="label label-info"><?php echo xlt('Online'); ?> : {{ online.total || '0' }}</span></h4>
1016 <label ng-repeat="ol in onlines | unique : 'username'">
1017 <input type="checkbox" data-checklist-model="onlines" data-checklist-value="ol"> {{ol.username}}
1018 </label>
1019 </div>
1020 </div>
1022 <div class="modal modal-wide fade" id="popeditor">
1023 <div class="modal-dialog modal-lg">
1024 <div class="modal-content">
1025 <form>
1026 <div class="modal-header">
1027 <button type="button" class="close" data-dismiss="modal">
1028 <span aria-hidden="true">&times;</span>
1029 <span class="sr-only"><?php echo xlt('Close'); ?></span>
1030 </button>
1031 <h4 class="modal-title"><?php echo xlt('Style your messsage and/or add Image/Video'); ?></h4>
1032 </div>
1033 <div class="modal-body">
1034 <summernote config="options"></summernote>
1035 </div>
1036 <div class="modal-footer">
1037 <button type="button" class="btn btn-sm" data-dismiss="modal"><?php echo xlt('Dismiss'); ?></button>
1038 <button type="button" class="btn btn-success" data-dismiss="modal" ng-click="saveedit()"><?php echo xlt('Send It'); ?></button>
1039 </div>
1040 </form>
1041 </div>
1042 </div>
1043 </div>
1044 <div class="modal" id="clear-history">
1045 <div class="modal-dialog">
1046 <div class="modal-content">
1047 <form>
1048 <div class="modal-header">
1049 <button type="button" class="close" data-dismiss="modal">
1050 <span aria-hidden="true">&times;</span>
1051 <span class="sr-only"><?php echo xlt('Close'); ?></span>
1052 </button>
1053 <h4 class="modal-title"><?php echo xlt('Chat history'); ?></h4>
1054 </div>
1055 <div class="modal-body">
1056 <label class="radio"><?php echo xlt('Are you sure to clear chat history?'); ?></label>
1057 </div>
1058 <div class="modal-footer">
1059 <button type="button" class="btn btn-sm btn-default" data-dismiss="modal"><?php echo xlt('Cancel'); ?></button>
1060 <button type="button" class="btn btn-sm btn-primary" data-dismiss="modal" ng-click="clearHistory()"><?php echo xlt('Accept'); ?></button>
1061 </div>
1062 </form>
1063 </div>
1064 </div>
1065 </div>
1067 </body>
1068 </html>