1/*! 2 * Screets Chat X - Application 3 * Author: @screetscom 4 * 5 * COPYRIGHT © 2016 Screets d.o.o. All rights reserved. 6 * This is commercial software, only users who have purchased a valid 7 * license and accept to the terms of the License Agreement can install 8 * and use this program. 9 */ 10 11;(function () { 12 13 var W = window, 14 D = document, 15 title = D.title, 16 root = this, 17 prev_chat_x = root.ChatX, 18 // jQuery style selector for more complex queries (slower than getElementById) 19 $ = function ( selector ) { 20 return D.querySelector( selector ); 21 } 22 23 function ChatX( opts ) { 24 25 var self = this; 26 27 // Setup and establish options 28 this.opts = opts; 29 30 // Create real-time database 31 if( !opts.no_db ) { 32 this.db = new SCX_FB( this.opts ); // Firebase 33 34 // Get reference 35 this.ref = this.db.ref; 36 37 // Bind data events 38 this._bind_data_events(); 39 } 40 41 42 // User data 43 this.user = {}; 44 this.user_id = ''; 45 this.username = ''; 46 47 // Useful variables 48 this.retina = this._is_retina(); 49 50 // Sounds 51 this.sounds = {}; 52 this._add_sound( 'connected' ); 53 this._add_sound( 'disconnected' ); 54 this._add_sound( 'new-msg' ); 55 this._add_sound( 'new-ntf' ); 56 this._add_sound( 'new-visitor' ); 57 58 // Some useful regexes 59 this.url_pattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; 60 this.pseudo_url_pattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim; 61 62 } 63 64 // 65 // Internal methods 66 // ------------------ 67 68 ChatX.prototype = { 69 70 /** 71 * Bind data events 72 */ 73 _bind_data_events : function() { 74 75 this.db.on( 'user-update', this._on_user_update.bind( this ) ); 76 77 }, 78 79 80 /** 81 * Listen user updates 82 */ 83 _on_user_update : function( user ) { 84 85 // User is deleted! 86 if( !user || !user.id ) { 87 88 // Disconnect for 89 if( !this.opts.is_frontend ) { 90 Firebase.goOffline(); 91 } 92 93 setTimeout( function() { 94 W.location.reload(); 95 }, 0 ); 96 97 return; 98 } 99 100 // Update our current user state and render latest user name 101 this.user = user; 102 103 // Update user id 104 this.user_id = user.id.toString(); 105 this.username = user.name.toString(); 106 107 // FIXME! 108 // Update our interface to reflect which users are muted or not. 109 /* 110 var mutedUsers = this._user.muted || {}; 111 $('[data-event="firechat-user-mute-toggle"]').each(function(i, el) { 112 var userId = $(this).closest('[data-user-id]').data('user-id'); 113 $(this).toggleClass('red', !!mutedUsers[userId]); 114 }); 115 116 // Ensure that all messages from muted users are removed. 117 for (var userId in mutedUsers) { 118 $('.message[data-user-id="' + userId + '"]').fadeOut(); 119 } 120 */ 121 }, 122 123 /** 124 * Setup UI for operators 125 */ 126 _setup_op_ui : function() {}, 127 128 /** 129 * Add sound 130 */ 131 _add_sound : function( name ) { 132 var source = D.createElement('source'), 133 filename = this.opts.plugin_url + '/assets/sounds/' + name; 134 135 this.sounds[name] = D.createElement('audio'); 136 137 if ( this.sounds[name].canPlayType('audio/mpeg;' ) ) { 138 source.type= 'audio/mpeg'; 139 source.src= filename + '.mp3'; 140 141 } else { 142 source.type= 'audio/ogg'; 143 source.src= filename + '.ogg'; 144 } 145 this.sounds[name].appendChild(source); 146 }, 147 148 /** 149 * Is URL an image 150 */ 151 _is_retina : function( uri ) { 152 153 if ( W.matchMedia ) { 154 var mq = W.matchMedia( "only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)" ); 155 return ( mq && mq.matches || ( W.devicePixelRatio > 1 ) ); 156 } 157 158 return false; 159 160 }, 161 162 /** 163 * Is URL an image 164 * 165 * Source: http://stackoverflow.com/a/19395606/272478 166 */ 167 _is_img : function( uri ) { 168 169 // Make sure we remove any nasty GET params 170 uri = uri.split('?')[0]; 171 172 // Moving on now 173 var parts = uri.split('.'), 174 extension = parts[parts.length-1], 175 imageTypes = ['jpg','jpeg','tiff','png','gif','bmp']; 176 177 if( imageTypes.indexOf( extension ) !== -1 ) { 178 return true; 179 } 180 181 }, 182 183 184 }; 185 186 // Run the script in "noConflict" mode 187 ChatX.noConflict = function noConflict() { 188 root.ChatX = prev_chat_x; 189 return ChatX; 190 }; 191 192 // Export the object as global 193 root.ChatX = ChatX; 194 195 /**
196 * Exposes internal chat bindings via this external interface 197 */ 198 ChatX.prototype.on = function( event_type, callback ) { 199 this.db.on( event_type, callback ); 200 }; 201 202 /** 203 * Initialize an authenticated session with a user id and name. 204 * This method assumes that the underlying Firebase reference has 205 * already been authenticated 206 */ 207 ChatX.prototype.set_user = function( user_id, username ) { 208 209 var self = this; 210 211 // Initialize data events 212 self.db.set_user( user_id, username, function( user ) { 213 214 self.user = user; 215 216 // Bind operator UI events 217 if( self.db.is_op ) { 218 self._setup_op_ui(); 219 } 220 221 // Resume session (don't resume when OP is in front-end) 222 if( !( self.opts.is_frontend && self.db.is_op ) ) { 223 self.db.resume_session(); 224 } 225 226 }); 227 228 }; 229 230 /** 231 * Add message into chat conversation 232 */ 233 ChatX.prototype.add_msg = function( chat_id, raw_msg, reverse_direction ) { 234 235 var obj_id = ['scx-cnv-',chat_id].join(''), 236 $cnv = D.getElementById( obj_id ), 237 pos = ( reverse_direction ) ? 'afterbegin' : 'beforeend', 238 cls = []; 239 240 if( !$cnv ) return; 241 242 // Message data 243 var is_you = ( raw_msg.user_id == this.db.user_id ), 244 data = { 245 id : raw_msg.id, 246 user_id : raw_msg.user_id, 247 name : raw_msg.name, 248 avatar : this.render( 'avatar', raw_msg.avatar ), 249 type : raw_msg.type, 250 time : this.format_time( raw_msg.timestamp ), 251 msg : raw_msg.msg || '', 252 self_msg : is_you 253 }; 254 255 // Sanitize the message 256 data.msg = this.sanitize( raw_msg.msg ); 257 258 // Is it you? 259 var _msg_owner = (is_you ) ? 'scx-you' : 'scx-other'; 260 cls.push( _msg_owner ); 261 262 // Is unread message 263 if( raw_msg.unread ) { 264 cls.push( 'scx-new' ); 265 } 266 267 // Include type 268 cls.push( 'scx-type-' + raw_msg.type ); 269 270 data._cls = cls.join(' '); 271 272 $cnv.insertAdjacentHTML( pos, this.render( 'chat-msg', data ) ); 273 274 // Scroll down 275 if( !reverse_direction ) { 276 $cnv.scrollTop = 10000; 277 } 278 279 }; 280 281 /** 282 * Play a sound 283 */ 284 ChatX.prototype.play = function( name ) { 285 this.sounds[name].play(); 286 }; 287 288 /** 289 * Create notification on UI 290 */ 291 ChatX.prototype.ntf = function( id, msg, type, section, ico, duration ) { 292 293 var self = this, 294 $ntf = ''; 295 296 // Update section 297 section = section || 'main'; 298 section = [ '.scx-ntf-', section ].join(''); 299 300 var $all_ntf = D.querySelectorAll( section ); 301 302 if( $all_ntf ) { 303 304 for( var i=0; i < $all_ntf.length; i++ ) { 305 306 $ntf = $all_ntf[i]; 307 308 // Include id 309 $ntf.id = [ 'scx-ntf-', id ].join(''); 310 311 // Get icon 312 if( ico ) { 313 ico = self.render( 'ico', ico ); 314 } 315 316 // Render message 317 $ntf.innerHTML = self.render( 'ntf', { 318 msg: msg, 319 type: type, 320 ico: ico 321 }); 322 323 // Set classes 324 $ntf.className += ' scx-active'; 325 326 // Hide notification after a specific time 327 if( duration ) { 328 root.setTimeout( function() { 329 self.hide_ntf( id ); 330 331 }, duration ); 332 333 // Hide chat for only main notifications in the console 334 } else if( !self.opts.is_frontend && section === '.scx-ntf-main' ) { 335 $ntf.addEventListener('click', function(e) { 336 self.remove_class( this, 'scx-active' ); 337 }); 338 } 339 340 } 341 342 } 343 344 }; 345 346 /** 347 * Create notification on UI 348 */ 349 ChatX.prototype.hide_ntf = function( id ) { 350 351 id = [ 'scx-ntf-', id ].join(''); 352 $ntf = D.getElementById( id ); 353 354 if( $ntf ) { 355 this.add_class( $ntf, 'scx-close' ); 356 this.remove_class( $ntf, 'scx-active' ); 357 } 358 359 }; 360 361 /** 362 * Remove a DOM object 363 */ 364 ChatX.prototype.remove_obj = function( id ) { 365 366 var $item = D.getElementById(id); 367 368 if( $item ) { 369 $item.parentNode.removeChild( $item ); 370 } 371 372 }; 373 374 /** 375 * Clear up UI from highlighted messages and counts 376 */ 377 ChatX.prototype.reset_ui = function() { 378 var $highlighted = D.getElementsByTagName('scx-highlighted'); 379 380 if( $highlighted ) { 381 for( var i=0; i < $highlighted.length; i++ ) { 382 this.remove_class( $highlighted[i], 'scx-highlighted' ); 383 } 384 } 385 }; 386 387 /** 388 * Convert HTML tags to text and replace plain URLs with links 389 * 390 * @param {string} 391 */ 392 ChatX.prototype.sanitize = function( str ) { 393 394 var links = str.match( this.url_pattern ) || [], 395 pseudo_links = str.match( this.pseudo_url_pattern ), 396 link = '', 397 href = '', 398 rx = ''; 399 400 // Merge links with pseudos 401 if( pseudo_links ) { 402 for( var i in pseudo_links ) { links.push( pseudo_links[i].trim() ); } 403 } 404 405 // Convert HTML tags and new lines to text 406 str = String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace( /\n/g, '<br>'); 407 408 // Twitter Emoji parse 409 str = twemoji.parse( str ); 410 411 for( var i=0; i<links.length; ++i ) { 412 link = links[i]; 413 414 // Clean http:// part from url 415 href = link.replace( /.*?:\/\//g, "" ); 416 417 rx = new RegExp( link, 'g' ); 418 419 str = str.replace( rx, this.render( 'msg-link', href ) ); 420 421 } 422 423 return str; 424 425 }; 426 427 /** 428 * Get time difference between two dates 429 * 430 */ 431 ChatX.prototype.time_diff = function( first, sec, return_type ) { 432 433 var diff = (first - sec); // milliseconds between two dates 434 435 switch( return_type ) { 436 437 case 'ms': // milliseconds 438 return diff; 439 440 case 'm': // minutes 441 return Math.round(((diff % 86400000) % 3600000) / 60000); 442 443 case 'h': // hours 444 return Math.round((diff % 86400000) / 3600000); 445 446 case 'd': // days 447 return Math.round(diffMs / 86400000); 448 449 } 450 }; 451 452 /** 453 * Given a timestamp, format it in the form hh:mm am/pm. Defaults to now 454 * if the timestamp is undefined. 455 * 456 * @param {Number} timestamp 457 * @param {string} date 458 * @return {string} 459 */ 460 ChatX.prototype.format_time = function( timestamp, full ) { 461 462 var date = (timestamp) ? new Date(timestamp) : new Date(), 463 year = date.getFullYear(),
464 month = date.getMonth() + 1, 465 day = date.getDate(), 466 hours = date.getHours() || '0', 467 minutes = '' + date.getMinutes(); 468 469 minutes = ( minutes.length < 2 ) ? '0' + minutes : minutes; 470 day = ( day.length < 2 ) ? '0' + day : day; 471 month = ( month.length < 2 ) ? '0' + month : month; 472 473 if( !full ) { 474 return '' + hours + ':' + minutes; 475 } else { 476 return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes; 477 } 478 479 }; 480 481 /** 482 * Add new class(es) 483 */ 484 ChatX.prototype.add_class = function( el, new_cls ) { 485 486 this.remove_class(el, new_cls ); 487 488 var cls = [el.className, new_cls].join(' '); 489 490 el.className = cls; 491 492 }; 493 494 /** 495 * Remove class(es) 496 */ 497 ChatX.prototype.remove_class = function( el, remove ) { 498 499 if( !el ) return; 500 var classes = remove.split( ' ' ), 501 rx = ''; 502 503 for( var i=0; i < classes.length; i++ ) { 504 rx = new RegExp( '(?:^|\\s)' + classes[i] + '(?!\\S)', 'g' ); 505 506 // Update class 507 el.className = el.className.replace( rx , '' ); 508 509 } 510 511 }; 512 513 /** 514 * Has class? 515 */ 516 ChatX.prototype.has_class = function( $el, selector ) { 517 518 var class_name = ' ' + selector + ' '; 519 520 if ( ( ' ' + $el.className + ' ' ).replace( /[\n\t]/g, ' ' ).indexOf( class_name ) > -1 ) { 521 return true; 522 } 523 return false; 524 525 }; 526 527 /** 528 * Check if a value exists in an array 529 * 530 * @param {string} 531 */ 532 ChatX.prototype.in_array = function( val, array ) { 533 534 var length = array.length; 535 536 for(var i = 0; i < length; i++) { 537 if( array[i] == val ) return true; 538 } 539 540 return false; 541 }; 542 543 /** 544 * Validate email field 545 */ 546 ChatX.prototype.is_email = function( email ) { 547 548 var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; 549 550 return re.test( email ); 551 552 }; 553 554 /** 555 * Send a post request to the server 556 */ 557 ChatX.prototype.post = function( url, data, callback ) { 558 559 var self = this, 560 xhr = new XMLHttpRequest(), 561 fd = new FormData(); 562 563 xhr.open( "POST", url, true ); 564 565 // Handle response 566 xhr.onreadystatechange = function() { 567 568 if ( xhr.readyState == 4 ) { 569 570 // Perfect! 571 if( xhr.status == 200 ) { 572 if( callback ) { callback( JSON.parse( xhr.responseText ) ); } 573 574 // Something wrong! 575 } else { 576 if( callback ) { callback( null ); } 577 } 578 } 579 }; 580 581 // Get data 582 for( var k in data ) { fd.append( k, data[k] ) ; } 583 584 // Initiate a multipart/form-data upload 585 xhr.send( fd ); 586 587 }; 588 589 /** 590 * Render template 591 */ 592 ChatX.prototype.render = function( template, p ) { 593 594 var arr = []; 595 596 switch( template ) { 597 598 // Notification 599 case 'ntf': 600 arr = [ '<div class="scx-wrap scx-', p.type,'">', p.ico, p.msg, '</div>' ]; 601 break; 602 603 // Admin notification 604 case 'admin-ntf': 605 arr = [ '<div class="scx-wrap scx-', p.type,'">', p.ico, p.msg, '</div>' ]; 606 break; 607 608 // Icon 609 case 'ico': 610 arr = [ '<span class="scx-ico ',p,'"></span>' ]; 611 break; 612 613 // Normal link :) 614 case 'msg-link': 615 arr = [ '<a href="http://',p,'" target="_blank" rel="nofollow">', p,'</a>' ]; 616 break; 617 618 // Chat message 619 case 'chat-msg': 620 arr = [ '<div id="SCX-msg-',p.id,'" class="scx-msg scx-msg-',p.id,' ',p._cls,'">',p.avatar,'<div class="scx-msg-wrap"><span class="scx-msg-time">',p.time,'</span><span class="scx-msg-author">',p.name,'</span><span class="scx-msg-content">',p.msg,'</span></div></div>' ]; 621 break; 622 623 // Avatar 624 case 'avatar': 625 arr = [ '<span class="scx-user-avatar"><img src="',p,'" alt="" /></span>' ]; 626 break; 627 628 // Title 629 case 'title': 630 arr = [ '' ]; 631 break; 632 633 } 634 635 return arr.join(''); 636 637 }; 638 639 640})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.