') .addClass('fa') .addClass( ips.getSetting('soundEnabled') ? 'fa-volume-up' : 'fa-volume-off' ) ); // Set up main loop to check for new messages this._poller = setInterval( _.bind( this._getMessages, this ), this._polling * 1000 ); this._updater = setInterval( _.bind( this._ping, this ), this._chattingUpdate * 1000 ); // Charset this._charset = $('meta[charset]').attr('charset'); }, /** * Event handler for window focussing * * @returns {void} */ windowFocus: function () { this._windowHasFocus = true; this._cleanTitle(); }, /** * Event handler for window blurring * * @returns {void} */ windowBlur: function () { this._windowHasFocus = false; this._unreadCount = 0; }, /** * Event handler indicating the active tab has changed * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ tabShown: function (e, data) { this._activeTab = data.tabID; this._updateTabCount( data.tabID, 0 ); if( !_.isUndefined( this._unreadTabs[ data.tabID ] ) ){ this._unreadTabs[ data.tabID ] = 0; } }, /** * Handles an event from the emoticon controller indicating we need to insert it into our chat box * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ insertEmoticon: function (e, data) { var room = this.scope.find('[data-tab="' + data.editorID + '"]'); // Check there's text to show and check this is a valid chat window if( !data.text || !room.length ){ return; } // Only do it if this is the active tab if( this._activeTab != data.editorID ){ return; } // Find the textarea var textarea = room.find('textarea'); // Don't put a space before the emoticon if there's nothing in the textarea var startSpace = ( textarea.val() !== '' ); // Insert it at the cursor textarea.focus(); textarea.replaceSelectedText( ( startSpace ? ' ' : '' ) + data.text + ' ' ); }, /** * Responds to data sent by the server via our postMessage iframe * * @param {string} response Server response * @returns {void} */ windowMessage: function (e) { var response = e.originalEvent.data; // Don't do anything if kicked or response is empty if( this._kicked || !response.length ){ return; } // Ignore height notifications if( response.substr( 0, 7 ) == 'height=' ){ return false; } if( response.substr( 0, 9 ) == '__ready__' || response.substr( 0, 8 ) == '__post__' || response.substr( 0, 12 ) == '__moderate__' ){ //If this is a response to a new submission or mod action, just get the messages this._getMessages(); } else { // Otherwise we need to update the displayed messages this._updateMessages( response ); } }, /** * Watches keypresses on the textarea and responds to ENTER * * @param {event} e Event object * @returns {void} */ messageBoxKeypress: function (e) { if( e.keyCode == ips.ui.key.ENTER ){ this._sendChat( e ); } }, /** * Watches submit button clicks * * @param {event} e Event object * @returns {void} */ submitButtonClick: function (e) { e.currentTarget = $( e.currentTarget ).closest('.cChatCompose').find('textarea.cMessageTextarea'); this._sendChat( e ); }, /** * Handles a selection from a user menu * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ userMenu: function (e, data) { var userRow = $( data.triggerElem ); var userID = userRow.attr('data-userID'); var forumUserID = userRow.attr('data-forumUserID'); var username = userRow.attr('data-username'); switch( data.selectedItemID ){ case 'private': data.originalEvent.preventDefault(); if( !ips.getSetting('private') ){ return; } this._doPrivateChat( forumUserID, userID, username ); break; case 'ignore': data.originalEvent.preventDefault(); this._ignoreUser( forumUserID, userID, username ); break; case 'unignore': data.originalEvent.preventDefault(); this._unignoreUser( forumUserID, userID, username ); break; case 'kick': data.originalEvent.preventDefault(); if( !ips.getSetting('moderator') ){ return; } this._kickUser( forumUserID, userID, username ); break; case 'ban': data.originalEvent.preventDefault(); if( !ips.getSetting('moderator') ){ return; } this._kickUser( forumUserID, userID, username ); this._banUser( forumUserID, userID, username ); break; } }, /** * Toggles notification sounds * * @param {event} e Event object * @returns {void} */ toggleSound: function (e) { e.preventDefault(); this.scope .find('[data-action="toggleSound"] .fa') .replaceWith( $('') .addClass('fa') .addClass( ips.getSetting('soundEnabled') ? 'fa-volume-off' : 'fa-volume-up' ) .removeClass( ips.getSetting('soundEnabled') ? 'fa-volume-up' : 'fa-volume-off' ) ); ips.setSetting( 'soundEnabled', !ips.getSetting('soundEnabled') ); ips.utils.db.set( 'chat', 'sounds', ips.getSetting('soundEnabled') ); // Show a flash message ips.ui.flashMsg.show( ips.getSetting('soundEnabled') ? ips.getString('soundEnabled') : ips.getString('soundDisabled') ); }, /** * User has clicked x to close a tab * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ closeTab: function (e, data) { var tab = $( e.currentTarget ).closest('.ipsTabs_item'); this._doCloseTab( tab ); }, toggleImage: function (e) { e.preventDefault(); var container = $( e.currentTarget ).closest('.cChatParsedImage'); if( container.attr('data-isHidden') ){ container .removeAttr( 'data-isHidden' ) .find('img') .show() .end() .find('[data-role="imageHidden"]') .hide() .end() .find('.fa') .removeClass('fa-caret-down') .addClass('fa-caret-up'); } else { container .attr( 'data-isHidden', true ) .find('img') .hide() .end() .find('[data-role="imageHidden"]') .show() .end() .find('.fa') .addClass('fa-caret-down') .removeClass('fa-caret-up'); } }, /** * Creates the notification badge on the chat tab * * @returns {void} */ _createBadge: function () { // This is now added with a hook and doesn't need to be done here //$('.elNavigation_app_chat_Chat').append( $('').addClass('ipsNotificationCount').text('0') ); }, /** * Sets up the main chatroom * * @returns {void} */ _setUpMainPanel: function () { var tabTemplate = ips.templates.render( 'chat.tabs.tab', { roomID: 'chatroom', roomTitle: ips.getString('chatroom'), tooltip: ips.getString('chatroomTooltip'), close: false, active: true }); var panelTemplate = ips.templates.render( 'chat.tabs.main', { roomID: 'chatroom', roomTitle: ips.getString('chatroom'), emoticons: ips.templates.render('core.editor.emoticons', { id: 'elEmoticons_chatroom', editor: 'chatroom' }), loading: true }); // Append it to the tab bar this.scope.find('#elChatTabsContent').append( panelTemplate ); this.scope.find('#elChatTabBar').append( tabTemplate ); this.scope.find('#elChatTabs') .attr( 'data-ipsTabBar', '' ) .attr( 'data-ipsTabBar-contentarea', '#elChatTabsContent' ); $( document ).trigger( 'contentChange', [ this.scope.find('#elChatWrapper') ] ); }, /** * Removes a tab * * @param {element} tabToClose Tab element * @returns {void} */ _doCloseTab: function (tab) { if( !tab.length ){ return; } var tabParent = tab.closest('li'); var active = tab.hasClass('ipsTabs_activeItem'); var switchTo = null; var tabID = tab.attr('id'); if( active ){ // Find the previous, or next, or #chatroom tab switchTo = tabParent.prev().length ? tabParent.prev() : ( tabParent.next().length ) ? tabParent.next() : tabParent.siblings('#chatroom'); switchTo.find('> a').click(); } ips.utils.anim.go('fadeOutDown fast', tabParent) .done( function () { tabParent.remove(); }); this.scope.find('.ipsTabs_panel[data-tab="' + tabID + '"]').remove(); }, /** * Removes the (2) thing from the browser title * * @returns {void} */ _cleanTitle: function () { // Note that we use a setTimeout here because Chrome's blur and focus events are not reliable. When switching tabs in the browser // the events are fired twice, however the first event does not update the document title. By using a setTimeout we work around this behavior. if( document.title.match( /\(\d+\) (.+?)/gi ) ){ setTimeout( function(){ document.title = document.title.replace( /\(\d+\) (.+?)/gi, "$1" ) }, 200 ); } this._unreadCount = 0; }, /** * Starts a private chat * * @param {number} forumUserID Local forum user ID * @param {number} userID Chat user ID * @param {string} username Username * @returns {void} */ _doPrivateChat: function (forumUserID, userID, username) { // Not if it's us if( userID == ips.getSetting('userID') ){ Debug.error("Can't chat with yourself"); return; } // Also not if we're ignoring them if( !_.isUndefined( ips.getSetting('ignoredChats')[ userID ] ) ){ ips.ui.alert.show( { type: 'alert', icon: 'info', message: ips.getString('cantChatWithSelf'), callbacks: {} }); return; } // Do we have a window already? if( !this.scope.find('[data-roomid="private_' + userID + '"]').length ){ this._createTab( userID, forumUserID, username ); } // Click it to focus it this._switchToTab( 'private_' + userID ); }, /** * Ping server to stay online * * @returns {void} */ _ping: function () { var url = '?app=chat&module=chat&controller=action&do=ping'; var self = this; // Load the mini profile to show in the sidebar ips.getAjax()( url, { data: { id: ips.getSetting('userID') } }) .done( function (response) { // We don't need to do anything }) .fail(function(response){ if ( response.responseJSON.message ) { var containers = self._getAllStorageContainers(); containers.append( ips.templates.render('chat.system', { text: response.responseJSON.message, date: self._getChatTime( self._getCurrentTime() ) })); self._kicked = true; self._scrollToBottom( self._activeTab ); } }); }, /** * Ban a user * * @param {number} forumUserID Local forum user ID * @param {number} userID Chat user ID * @param {string} username Username * @returns {void} */ _banUser: function (forumUserID, userID, username) { // Not if it's us if( userID == ips.getSetting('userID') ){ Debug.error("Can't ban yourself"); return; } if( !ips.getSetting('moderator') ){ Debug.error("You aren't a moderator."); return; } if( !forumUserID ){ Debug.error("Can't ban guests."); return; } var url = '?app=chat&module=chat&controller=action&do=banUser'; // Send ajax request to backend if they're a member ips.getAjax()( url, { data: { id: forumUserID, block: 0 }, type: 'post' }); // Update menu choices this._syncMenuOptions( userID, forumUserID ); // Update last action this._lastAction = this._getCurrentTime(); }, /** * Kicks a user * * @param {number} forumUserID Local forum user ID * @param {number} userID Chat user ID * @param {string} username Username * @returns {void} */ _kickUser: function (forumUserID, userID, username) { if( !ips.getSetting('moderator') ){ Debug.error("You aren't a moderator."); return; } // Not if it's us if( userID == ips.getSetting('userID') ){ Debug.error("Can't kick yourself"); return; } this._sendMessageToServer({ action: 'kick', against: userID }); Debug.log( "Kicked " + username + " from the chatroom."); }, /** * unIgnore a user * * @param {number} forumUserID Local forum user ID * @param {number} userID Chat user ID * @param {string} username Username * @returns {void} */ _unignoreUser: function (forumUserID, userID, username) { // Not if it's us if( userID == ips.getSetting('userID') ){ Debug.error("Can't unignore yourself"); return; } var url = '?app=chat&module=chat&controller=action&do=blockUser'; var tmpID = forumUserID; if( forumUserID ){ // Send ajax request to backend if they're a member ips.getAjax()( url, { data: { id: forumUserID, block: 0 }, type: 'post' }); } else { // If they're a guest, add them to the local storage var ignoredGuests = ips.utils.db.get('chat', 'ignoredGuests') || []; if( _.indexOf( ignoredGuests, userID ) !== -1 ){ ignoredGuests = _.without( ignoredGuests, userID ); } ips.utils.db.set( 'chat', 'ignoredGuests', ignoredGuests ); tmpID = 'g_' + userID; } if( !_.isUndefined( ips.getSetting('ignoredChats')[ tmpID ] ) ){ delete ips.getSetting('ignoredChats')[ tmpID ]; } // Update menu choices this._syncMenuOptions( userID, forumUserID ); // Update last action this._lastAction = this._getCurrentTime(); }, /** * Ignore a user * * @param {number} forumUserID Local forum user ID * @param {number} userID Chat user ID * @param {string} username Username * @returns {void} */ _ignoreUser: function (forumUserID, userID, username) { // Not if it's us if( userID == ips.getSetting('userID') ){ Debug.error("Can't ignore yourself"); return; } var user = ips.getSetting('forumIDMap')[ userID ]; if( _.isUndefined( user ) || !user.canBeIgnored ){ ips.ui.alert.show( { type: 'alert', icon: 'info', message: ips.getString('cantIgnoreUser', { user: username }), callbacks: {} }); return; } var url = '?app=chat&module=chat&controller=action&do=blockUser'; var tmpID = forumUserID; if( forumUserID ){ // Send ajax request to backend if they're a member ips.getAjax()( url, { data: { id: forumUserID, block: 1 }, type: 'post' }); } else { // If they're a guest, add them to the local storage var ignoredGuests = ips.utils.db.get('chat', 'ignoredGuests') || []; ignoredGuests.push( userID ); ips.utils.db.set( 'chat', 'ignoredGuests', ignoredGuests ); tmpID = 'g_' + userID; } ips.getSetting('ignoredChats')[ tmpID ] = tmpID; // Update menu choices this._syncMenuOptions( userID, forumUserID ); // Update last action this._lastAction = this._getCurrentTime(); }, /** * Syncs the menu options to current data, showing/hiding items as needed * * @param {number} userID ID of the user menu to sync * @returns {void} */ _syncMenuOptions: function (userID, forumUserID) { var menu = this.scope.find('.cChatUserMenu[data-userID="' + userID + '"]'); // If we aren't a moderator, we can get rid of all those options if( !ips.getSetting('moderator') ){ menu.find('[data-ipsMenuValue="kick"], [data-ipsMenuValue="ban"], [data-role="modSep"]').remove(); } // If we can't do privates, get rid of that too if( !ips.getSetting('private') ){ menu.find('[data-ipsMenuValue="private"], [data-role="privateSep"]').remove() } // Are we ignoring them? var ignoring = !_.isUndefined( ips.getSetting('ignoredChats')[ forumUserID ? forumUserID : 'g_' + userID ] ); var isSelf = userID == ips.getSetting('userID'); // Show/hide the appropriate option, or hide both if it's us menu .find('[data-ipsMenuValue="unignore"]') .toggle( ignoring && !isSelf ) .end() .find('[data-ipsMenuValue="ignore"]') .toggle( !ignoring && !isSelf ); }, /** * Switches the view to a different tab * * @param {string} tabID ID of the tab to create * @returns {void} */ _switchToTab: function (tabID) { this.scope .find( '#' + tabID ) .click() .end() .find('[name="message"]') .focus(); }, /** * Creates a new tab in the chatroom * * @param {string} tabID ID of the tab to create * @param {string} tabTitle Text shown on the tab * @returns {void} */ _createTab: function (userID, forumUserID, tabTitle) { var tabTemplate = ips.templates.render( 'chat.tabs.tab', { roomID: 'private_' + userID, roomTitle: tabTitle, tooltip: ips.getString('privateChatWith', { user: tabTitle }), close: true }); var panelTemplate = ips.templates.render( 'chat.tabs.panel', { roomID: 'private_' + userID, roomTitle: tabTitle, emoticons: ips.templates.render('core.editor.emoticons', { id: 'elEmoticons_private_' + userID, editor: 'private_' + userID }) }); // Append it to the tab bar this.scope.find('#elChatTabBar').append( tabTemplate ); this.scope.find('#elChatTabsContent').append( panelTemplate ); var userInfoArea = this.scope.find( '[data-tab="private_' + userID + '"]' ).find('.cChatUserInfo'); var url = '?app=chat&module=chat&controller=action&do=loadUserInfo'; // Load the mini profile to show in the sidebar ips.getAjax()( url, { data: { id: forumUserID } }) .done( function (response) { userInfoArea.removeClass('ipsLoading').html( response ); }); }, /** * Sends a message the user has typed * This method differs slightly from the 3.x version because each chat tab * has its own textarea now, so we use that tab to determine which room to send * the message to. * * @param {event} e Event object * @returns {void} */ _sendChat: function (e) { if( e ){ e.preventDefault(); } // Get message, trim and clean it var message = this._cleanMessage( $.trim( $( e.currentTarget ).val() ) ); var tab = $( e.currentTarget ).closest('[data-tab]').attr('data-tab'); var tabID = $( e.currentTarget ).closest('[data-tab]').attr('data-tab').replace('private_', ''); if( !message || !tabID ){ Debug.log("No message or no tab"); return; } var extra = ''; var data = { lmsg: this._lastMsgId, message: message }; if( tab == 'chatroom' ){ data.action = 'post'; extra = '0'; } else { data.action = 'private'; data.toUser = tabID; extra = 'private=' + tabID; } if( this._sendMessageToServer( data ) ){ this._scrollToBottom( tab ); // Manually send our own chats to the screen here var post = this._escapeHTML( message ); this._bypassFlag = true; this._updateMessages( "1,0~~||~~" + Math.round( new Date().getTime() / 1000 ) + ",1," + ips.getSetting('username').replace( ',', '~~#~~' ) + "," + post + "," + extra + "," + ips.getSetting('userID') + "~~||~~" ); this._bypassFlag = false; } // Clear the field $( e.currentTarget ).val(''); this._scrollToBottom( tab ); this._lastAction = this._getCurrentTime(); }, /** * Get messages from the server * * @returns {void} */ _getMessages: function () { var curtime = this._getCurrentTime(); this._checkInactivity( curtime ); if( this._kicked ){ clearInterval( this._poller ); clearInterval( this._updater ); } // If current time - last polled time is less than the polling timeperiod, // skip this iteration (prevent DOS when user submits many messages quickly) if( ( curtime - this._lastPolled ) < this._polling ){ return false; } this._lastPolled = curtime; this._sendMessageToServer( { msg: this._lastMsgId }); }, /** * Checks whether we've been inactive longer than allowed * * @returns {void} */ _checkInactivity: function (timestamp) { if( !ips.getSetting('inactiveKick') ){ return; } if( timestamp - this._lastAction > ips.getSetting('inactiveKick') ){ var whateverItWas = this._bypassFlag; this._bypassFlag = true; this._updateMessages( "0," + this._lastMsgId + "~~||~~" + this._getCurrentTime() + ",5," + ips.getSetting('username').replace( ',', '~~#~~' ) + ",,1_" + ips.getSetting('userID') + "," + ips.getSetting('userID') + "~~||~~" ); this._bypassFlag = whateverItWas; this._kicked = true; } return; }, /** * Sends a system message to the server * * @param {object} data Key/values to send to the server * @returns {void} */ _sendMessageToServer: function (data) { var url = 'http' + ( this._secure ? 's' : '' ) + '://' + ips.getSetting('serverHost'); var self = this; // Don't send any messages if kicked if( this._kicked ){ return false; } data = _.defaults( data, { server: ips.getSetting('serverHost'), path: ips.getSetting('serverPath'), room: ips.getSetting('roomID'), user: ips.getSetting('userID'), access_key: ips.getSetting('accessKey'), charset: this._charset }); // Send request Debug.log( "Sending: " + JSON.stringify( data ) ); this._iframeWindow.postMessage( JSON.stringify( data ), url ); return true; }, /** * Plays a sound notification * * @param {string} soundName Key of the sound to play * @returns {void} */ _playSound: function (soundName) { if( _.isUndefined( ips.getSetting('sounds')[ soundName ] ) || !ips.getSetting('soundEnabled') ){ return; } var sound = new buzz.sound( ips.getSetting('baseURL') + 'applications/chat/interface/sounds/' + ips.getSetting('sounds')[ soundName ], { formats: [ "mp3" ] }); sound.play(); }, /** * Updates the messages in the list * * @param {string} response Raw server response * @returns {void} */ _updateMessages: function (response) { if( !response ){ return; } // Stop the panels from loading this.scope.find('.cChatContainer').removeClass('ipsLoading'); // http://community.invisionpower.com/tracker/issue-35857-incompatibility if( response.substr( 0, 2 ) != '0,' && response.substr( 0, 2 ) != '1,' ){ return true; } // Messages are imploded on ~~||~~ var messages = response.split( "~~||~~" ); var state = { showOwn: false, initialEmpty: false, previousLastMsgID: this._lastMsgId, updatedCount: 0, alsoTo: null }; // Loop over each message for( var i = 0; i < messages.length; i++ ){ messages[ i ] = this._parseMessage( messages[ i ] ); var msgDetails = messages[ i ].split( ',' ); // First iteration is meta data: code,lastMessageId if( i === 0 ){ // Request failed if( msgDetails[0] != 1 ){ var containers = this._getAllStorageContainers(); containers.append( ips.templates.render('chat.system', { text: ips.getString('kickedFromServer'), date: this._getChatTime( this._getCurrentTime() ) })); // Kick to prevent further calls this._kicked = true; this._scrollToBottom( this._activeTab ); return true; } else { if( !this._bypassFlag && msgDetails[1] == this._lastMsgId ){ if( this._lastMsgId == 0 ){ this._bypassFlag = true; this._lastMsgId = 1; state.initialEmpty = true; msgDetails[1] = 2; msgDetails[2] = ips.getSetting('username'); msgDetails[3] = ''; msgDetails[4] = '1_' + ips.getSetting('userID'); msgDetails[5] = ips.getSetting('userID'); } else { return true; } } if( !this._bypassFlag ){ // If we refreshed the page, and all chat messages are being pulled, // we don't want to skip our own messages if( this._lastMsgId == 0 ) { state.showOwn = true; } this._lastMsgId = msgDetails[1]; } if( !state.initialEmpty ){ continue; } } } if( !state.previousLastMsgID && this._noBuffer ){ continue; } if( msgDetails[1] ){ // Don't add our own messages since we do that automatically upon submit if( !this._bypassFlag && msgDetails[5] == ips.getSetting('userID') && ( msgDetails[1] == 1 || msgDetails[1] == 3 ) && !state.showOwn ){ continue; } var thisRoomID = 'chatroom'; if( msgDetails[1] == 1 ){ // Is this a private chat? if( msgDetails[4] ){ var userDetails = msgDetails[4].split( '=' ); if( userDetails[1] ){ var user = msgDetails[5] == ips.getSetting('userID') ? userDetails[1] : msgDetails[5]; var forumUser = ips.getSetting('forumIDMap')[ user ]; if( _.isUndefined( forumUser ) ){ continue; } if( !this.scope.find('[data-tab="private_' + user + '"]').length ){ var blocked = 0; if( forumUser.forumUserID ){ blocked = ips.getSetting('ignoredChats')[ forumUser.forumUserID ]; } else { blocked = ips.getSetting('ignoredChats')[ 'g_' + user ]; } if( blocked ){ Debug.log('blocked'); continue; } this._createTab( user, forumUser.forumUserID, forumUser.username ); } thisRoomID = 'private_' + user; } } } var ulList = this._getStorageContainer( thisRoomID ); var scrollBottom = this._scrollIsAtBottom( thisRoomID ); // Format date from timestamp var itemDate = this._getChatTime( msgDetails[0] ); msgDetails[2] = msgDetails[2].replace( '~~#~~', "," ); msgDetails[2] = this._unCleanMessage( msgDetails[2] ); msgDetails[4] = msgDetails[4].replace( '~~#~~', "," ); msgDetails[3] = msgDetails[3].replace( '~~#~~', "," ); msgDetails[3] = this._unCleanMessage( msgDetails[3] ); msgDetails[3] = this._parseEmoticonsAndBbcode( msgDetails[3], msgDetails[5] ); // /me commands if( msgDetails[1] == 1 ){ if( !_.isUndefined( msgDetails[3] ) && msgDetails[3].match( /^\/me /gi ) ){ msgDetails[1] = 3; msgDetails[3] = msgDetails[3].replace( /^\/me /gi, '' ); } } // Handle the message appropriately // Calls a function appropriate for the type of message being handled var handlers = ["normal", "userStatus", "me", "system", "modAction"]; if( !_.isUndefined( handlers[ ( msgDetails[ 1 ] - 1 ) ] ) ){ var handlerReturn = this._handlers[ handlers[ ( msgDetails[ 1 ] - 1 ) ] ].call( this, msgDetails, thisRoomID, itemDate, state ); } if( handlerReturn === false ){ continue; } if( handlers[ ( msgDetails[ 1 ] - 1 ) ] == 'system' ){ // Update all windows ulList = this._getAllStorageContainers(); } else if( handlers[ ( msgDetails[ 1 ] - 1 ) ] == 'userStatus' ){ // Update the main window, and any private windows ulList = this._getAllStorageContainers( msgDetails[4].split( '_' )[ 1 ] ); } // Update current window ulList.append( handlerReturn ); // Make sure new list has less li items than max allows if( ips.getSetting('maxMessages') > 0 ){ var rowCount = ulList.find('> li'); // Got more than we're allowed? if( rowCount.length > ips.getSetting('maxMessages') ){ rowCount.slice( 0, rowCount.length - ips.getSetting('maxMessages') ).remove(); } } // Update the last time if( !this._bypassFlag ){ this._lastTime = itemDate; } if( scrollBottom || !this._doneInitialLoad ){ this._scrollToBottom( thisRoomID ); } } } // Handle unread counts if( !this._windowHasFocus && this._unreadCount > 0 ){ if( document.title.match( /\(\d+\) (.+?)/gi ) ){ document.title = document.title.replace( /\(\d+\) (.+?)/gi, "(" + this._unreadCount + ") $1" ); } else { document.title = "(" + this._unreadCount + ") " + document.title; } } // Update tab counts this._updateTabUnreadCount(); // Play sound if needed if( state.updatedCount ){ this._playSound('message'); } // Reset bypass flag if this was initial pull override if( state.initialEmpty ){ this._bypassFlag = false; } this._doneInitialLoad = true; }, /** * Returns a time string based on given timestamp, or 'now' if none * * @param [number] timestamp Optional timestamp on which time is based * @returns {string} */ _getChatTime: function (timestamp) { var itemDate = new Date(); var secs = parseInt( timestamp ) ? parseInt( timestamp ) : parseInt( new Date().getTime() ); itemDate.setTime( secs * 1000 ); var hours = itemDate.getHours(); var mins = itemDate.getMinutes() + ""; // This makes sure we have leading 0 if( mins.length == 1 ){ mins = "0" + mins; } // Normalize dates for AM/PM instead of 24 hour if( ips.getSetting('user24hour') ){ itemDate = hours + ':' + mins; } else { if( hours > 12 ){ itemDate = ( hours - 12 ) + ':' + mins + ' ' + ips.getString('time_pm'); } else if( hours == 0 ){ itemDate = 12 + ':' + mins + ' ' + ips.getString('time_am'); } else if( hours == 12 ){ itemDate = hours + ':' + mins + ' ' + ips.getString('time_pm'); } else { itemDate = hours + ':' + mins + ' ' + ips.getString('time_am'); } } return itemDate; }, /** * Returns the chat list element for a given container ID * * @param {string} containerID ID to fetch * @returns {element} */ _getStorageContainer: function (containerID) { return this.scope.find('[data-tab="' + containerID + '"] .cChatContainer'); }, /** * Returns all chat list elements * * @returns {element} */ _getAllStorageContainers: function (filter) { if( !filter ){ return this.scope.find('[data-tab] .cChatContainer'); } else { return this.scope.find('[data-tab="chatroom"] .cChatContainer, [data-tab="private_' + filter + '"] .cChatContainer'); } }, _updateTabUnreadCount: function () { // @todo }, /** * Formats a username * * @returns {element} */ _formatName: function (userID, username) { var details = ips.getSetting('forumIDMap')[ userID ]; if( !_.isUndefined( details ) ){ return details.prefix.replace( /__DBQ__/g, '"' ) + username + details.suffix.replace( /__DBQ__/g, '"' ); } return username; }, /** * Builds the initial user list * * @param {number} userID Chat user id * @param {string} username User's name * @param {number} forumUserID Forum user id * @returns {void} */ _buildInitialList: function () { this.scope.find('#elUserList').html( this.scope.find('#elInitialUserList').html() ); this.scope.find('#elInitialUserList').remove(); var self = this; this.scope.find('#elUserList li').each( function(){ self._syncMenuOptions( $(this).find('[data-userID]').attr('data-userID'), $(this).find('[data-forumUserID]').attr('data-forumUserID') ); }); this._syncTotalChatters(); }, /** * Adds a user to the list after fetching info via ajax * * @param {number} userID Chat user id * @param {string} username User's name * @param {number} forumUserID Forum user id * @returns {void} */ _addUserToList: function (userID, username, forumUserID) { var userlist = this.scope.find('#elUserList'); var url = ips.getSetting('baseURL') + 'index.php?app=chat&module=chat&controller=action&do=addUser'; var self = this; if( userlist.find('[data-userID="' + userID + '"]').length ){ return; } // Fetch the data ips.getAjax()( url, { dataType: 'json', data: { id: forumUserID, user: userID } }) .done( function (response) { // Update forum map ips.getSetting('forumIDMap')[ userID ] = response.user; // Insert in alphabetical order var done = false; userlist.find('[data-userID]').each( function () { var name = $( this ).attr('data-userID'); if( ips.getSetting('username') < name ){ $( this ).before( response.html ); done = true; } }); if( !done ){ userlist.append( response.html ); } self._syncMenuOptions( userID, forumUserID ); $( document ).trigger( 'contentChange', [ userlist ] ); // Play enter sound self._playSound('enter'); self._syncTotalChatters(); }); }, /** * Removes a chatter from the user list * * @param {number} userID Chat user id * @param {string} username User's name * @param {number} forumUserID Forum user id * @returns {void} */ _removeUserFromList: function (userID, username, forumUserID) { var userlist = this.scope.find('#elUserList'); var row = userlist.find('#elChatUserRow_' + userID ); var self = this; if( !row.length ){ return; } // Remove from list userlist.find('[data-userID="' + userID + '"]').slideUp( function (){ row.remove(); self._syncTotalChatters(); }); // Play the leaving sound self._playSound('leave'); }, /** * Syncs the '2 users chatting' and the global chat tab counts * * @returns {void} */ _syncTotalChatters: function () { var userlist = this.scope.find('#elUserList'); var total = userlist.find('> li').length; // Update total this.scope.find('[data-role="onlineChatters"]').html( ips.pluralize( ips.getString('usersChatting'), total ) ); // Update global chat tab $('.ipsNavBar_primary [data-navapp="chat"] .ipsNotificationCount').text( total ); }, /** * Swaps out certain characters with alternatives we can make safe * * @param {string} message Raw message * @returns {string} */ _cleanMessage: function (message) { return message .replace( /\r/g, '' ) .replace( /\n/g, "__N__" ) .replace( /,/g, "__C__" ) .replace( /=/g, "__E__" ) .replace( /\+/g, "__PS__" ) .replace( /&/g, "__A__" ) .replace( /%/g, "__P__" ); }, /** * Swaps out our spcial characters back to their proper one * * @param {string} message Raw message * @returns {string} */ _unCleanMessage: function (message) { return message .replace( /__PS__/g, "+" ) .replace( /__E__/g, "=" ) .replace( /__P__/g, "%" ) .replace( /__A__/g, "&" ) .replace( /__C__/g, "," ) .replace( /__N__/g, "
" ); }, /** * Parses our emoticons and bbcode from chats * * @param {string} message Raw message * @param {number} chatUserID Chat user ID number * @returns {string} */ _parseEmoticonsAndBbcode: function (message, chatUserID) { var self = this; // Basics message = message.replace( /\[b\](.+?)\[\/b\]/gi, "$1" ); message = message.replace( /\[i\](.+?)\[\/i\]/gi, "$1" ); message = message.replace( /\[u\](.+?)\[\/u\]/gi, "$1" ); // Emoticons // @todo this line still needed? message = message.replace( '!', '!' ); _.each( ips.getSetting('chatEmoticons'), function (url, emoticon) { var img = $('').attr({ 'src': url['image'], 'data-emoticon': 'true' }); if ( url['width'] ) { img.attr( 'width', url['width'] ); } if ( url['height'] ) { img.attr( 'height', url['width'] ); } if ( url['image_2x'] ) { img.attr( 'srcset', url['image_2x'] ); } img = "$1" + $('
').append( img ).html() + "$2"; var regexp = new RegExp( "(^|\\s)" + self._escapeRegex( emoticon ) + "(\\s|$)", "gi" ); while( message.match( regexp ) ){ message = message.replace( regexp, img ); } }); // Bad words var groupID = 0; var canBypassBadwords = false; var canBypassBadwords = false; if( chatUserID ){ var forumUser = ips.getSetting('forumIDMap')[ chatUserID ]; if( !_.isUndefined( forumUser ) ){ groupID = forumUser.groupID canBypassBadwords = forumUser.canBypassBadwords; } } // Only do this if this group can't bypass badword filters if( !canBypassBadwords ){ var replaceMap = {}; var iteration = 0; // Loop through each badword to set up the replacement _.each( ips.getSetting('badwords'), function (info, badword) { var regex = null; var matches = null; if( _.isUndefined( info.swop ) ){ info.swop = '#####'; } // Apparently we have to treat 'ass' differently because of the HTML class attribute if( badword == 'ass' ){ var reversed = esrever.reverse( message ); if( info.m_exact ){ // Loose regex = new RegExp( '(\\b|!|\\?|\\.|,|$)' + self._escapeRegex( reversed ) + '(?!lc)(^|\\b|\\s)', "gi" ); } else { // Exact regex = new RegExp( self._escapeRegex( reversed ) + '(?!lc)', "gi" ); } while( matches = regex.exec( message ) ){ if( matches !== null ){ if( info.m_exact ){ replaceMap[ "__BW" + iteration + "__" ] = esrever.reverse( matches[2] ) + info.swop + esrever.reverse( matches[1] ); } else { replaceMap[ "__BW" + iteration + "__" ] = info.swop; } reversed = reversed.replace( matches[0], "__" + iteration + "WB__" ); iteration++; } } message = esrever.reverse( reversed ); return; } else { // Is this a loose replacement or exact match? if( info.m_exact ){ // Loose regex = new RegExp( '(^|\\b|\\s)' + self._escapeRegex( badword ) + '(\\b|!|\\?|\\.|,|$)', "gi" ); } else { // Exact regex = new RegExp( self._escapeRegex( badword ), "gi" ); } // Iterate through the matches while( matches = regex.exec( message ) ){ if( matches !== null ){ if( info.m_exact ){ replaceMap[ "__BW" + iteration + "__" ] = matches[1] + info.swop + matches[2]; } else { replaceMap[ "__BW" + iteration + "__" ] = info.swop; } message = message.replace( matches[0], "__BW" + iteration + "__" ); iteration++; } } } }); // Now make the replacements for real _.each( replaceMap, function (value, key) { message = message.replace( key, value ); }); } return message; }, /** * Parses a raw message * * @param {string} Raw message * @returns {string} */ _parseMessage: function (message) { message = message.replace( /%20/gi, ' ' ); message = message.replace( /%23/gi, '#' ); message = message.replace( /%22/gi, '"' ); message = message.replace( /%3C/gi, '<' ); message = message.replace( /%3E/gi, '>' ); message = message.replace( /\/gi, '>' ); // This is here mainly for Safari. In Safari, it automatically encodes chars // and they are not valid for decodeURI/decodeURIComponent. // Example: teraßyte becomes tera%DFyte // This function will decode those characters message = message.replace( /\%([a-zA-Z0-9]{2})/gi, function( hex ) { return String.fromCharCode( parseInt( hex.replace( '%', '' ), 16 ) ); }); return message; }, /** * Returns current time in seconds * * @returns {number} */ _getCurrentTime: function () { return parseInt( new Date().getTime().toString().substring( 0, 10 ) ); }, _scrollIsAtBottom: function (rowID) { var container = this._getStorageContainer( rowID ); if( container.scrollTop() + container.innerHeight() >= container.get(0).scrollHeight ){ Debug.log( "At bottom of " + rowID ); return true; } return false; }, /** * Tracks the number of unread messages * * @returns {void} */ _updateUnread: function (rowID, lastUserID, state) { state.updatedCount++; this._unreadCount++; if( rowID != this._activeTab ){ if( this._unreadTabs[ rowID ] ){ this._unreadTabs[ rowID ] = parseInt( this._unreadTabs[ rowID ] ) + 1; } else { this._unreadTabs[ rowID ] = 1; } } var self = this; _.each( this._unreadTabs, function (value, key) { if( key != self._activeTab ){ self._updateTabCount( key, value ); } }); this._lastUser[ rowID ] = lastUserID; }, /** * Changes the 'unread' count on the given tab. Pass 0 for count to remove the number. * * @param {string} roomID Tab/room ID * @param {number} count Number of unread messages * @returns {void} */ _updateTabCount: function (roomID, count) { var tab = this.scope.find('[data-roomid="' + roomID + '"] a'); if ( !tab.length ) { return; } var text = tab.html(); // Remove any existing count text = text.replace( /^\(\d+\) /i, '' ); if( count ){ text = "(" + count + ") " + text; } tab.html( text ); }, /** * Creates the iframe we'll use for cross-domain messaging * * @returns {void} */ _createIFrame: function () { this._iframe = $('