AjaxIM = function(options, actions) { if(this instanceof AjaxIM) { var self = this; // === {{{ defaults }}} === // // These are the available settings for Ajax IM, and the associated // defaults: // // * {{{pollServer}}} is the default URL to which all actions refer. It is // possible to specify certain action URLs separately (as is used with the // NodeJS server). // * {{{theme}}} is the name of the theme folder that defines the HTML and // CSS of the IM bar and chat boxes. Usually, themes are deposited in the // provided "themes" folder and specified by that path, e.g. {{{themes/default}}}. // Theme files within the theme folder must be named {{{theme.html}}} and // {{{theme.css}}}. var defaults = { pollServer: '', theme: 'themes/default' }; // === {{{AjaxIM.}}}**{{{settings}}}** === // // These are the settings for the IM. If particular options are not specified, // the defaults (see above) will be used. //These options will be defined // upon calling the initialization function, and not set directly.// this.settings = $.extend(defaults, options); // === {{{AjaxIM.}}}**{{{actions}}}** === // // Each individual action that the IM engine can execute is predefined here. // By default, it merely appends the action onto the end of the {{{pollServer}}} url, // however, it is possible to define actions individually. //The alternative actions // will be defined upon calling the initialization function, and not set directly.// // // Should you define an action at a different URL, Ajax IM will determine whether // or not this URL is within the current domain. If it is within a subdomain of // the current domain, it will set the document.domain variable for you, // to match a broader hostname scope; the action will continue to use {{{$.post}}} // (the default AJAX method for Ajax IM). // // On the other hand, should you choose a URL outside the current domain // Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid // cross-domain scripting issues. This means that a server on a different // port or at a different address will need to be able to handle GET // requests rather than POST requests (such as how the Node.JS Ajax IM // server works). this.actions = $.extend({ noop: this.settings.pollServer + '/app/noop', listen: this.settings.pollServer + '/app/listen', send: this.settings.pollServer + '/app/message', status: this.settings.pollServer + '/app/status', signoff: this.settings.pollServer + '/app/signoff' }, actions); // If Socket.IO is available, create a socket self.socket = null; $.getScript(this.settings.pollServer+'/socket.io/socket.io.js', function(){ self.socket = io(self.settings.pollServer); self.socket.on('client', function(event) { event = $.extend(true, {}, event); self.dispatchEvent(event); }); var event = {type: 'hello', from: this.username, sessionID: cookies.get('sessionid')}; self.sendEvent(event); }); // We load the theme dynamically based on the passed // settings. If the theme is set to false, we assume // that the user is going to load it himself. this.themeLoaded = false; if(this.settings.theme) { if(typeof document.createStyleSheet == 'function') document.createStyleSheet(this.settings.theme + '/theme.css'); else $('body').append(''); $('
').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip', function() { self.themeLoaded = true; self.setup(); } ); } else { this.themeLoaded = true; this.setup(); } // Allow a chatbox to be minimized $(document).on('click', '.imjs-chatbox', function(e) { e.preventDefault(); return false; }); $(document).on('click', '.imjs-chatbox .imjs-minimize', function() { $(this).parents('.imjs-selected').click(); }); // Setup message sending for all chatboxes $(document).on('keydown', '.imjs-chatbox .imjs-input', function(event) { var obj = $(this); // if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) { if(event.keyCode == 13) { self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } }).on('keyup', '.imjs-chatbox .imjs-input', function(event) { if(event.keyCode == 13) { // if($.browser.msie && $.browser.version < 8) { if(false) { var obj = $(this); self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } var obj = $(this); obj.val(''); obj.height(obj.data('height')); } }).on('keypress', '.imjs-chatbox .imjs-input', function(e) { var obj = $(this); obj.height(0); if(true) obj.height(0); if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) { obj.height(this.scrollHeight); } }); $(document).on('click', '.imjs-msglog', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.find('.imjs-input').focus(); }); // Create a chatbox when a buddylist item is clicked $(document).on('click', '.imjs-friend', function() { var chatbox = self._createChatbox($(this).data('friend')); if(chatbox.parents('.imjs-tab').data('state') != 'active') { chatbox.parents('.imjs-tab').click(); store.set(self.username + '-activeTab', $(this).data('friend')); } chatbox.find('.imjs-input').focus(); if(!(input = chatbox.find('.imjs-input')).data('height')) { // store the height for resizing later if (!input.height()) { input.height(16); } input.data('height', input.height()); } }); // Setup and hide the scrollers $('.imjs-scroll').css('display', 'none'); $(document).on('click', '#imjs-scroll-right', function() { var hiddenTab = $(this) .prevAll('#imjs-bar li.imjs-tab:hidden') .filter(function() { return ( $(this).data('state') != 'closed' && $(this).prev('#imjs-bar li.imjs-tab:visible').length ); }) .not('.imjs-default') .slice(-1) .css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1); } return false; }); $(document).on('click', '#imjs-scroll-left', function() { var hiddenTab = $(this) .nextAll('#imjs-bar li.imjs-tab:hidden') .filter(function() { return ( $(this).data('state') != 'closed' && $(this).next('#imjs-bar li.imjs-tab:visible').length ); }) .not('.imjs-default') .slice(-1) .css('display', ''); if(hiddenTab.length) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); $(this).html(parseInt($(this).html()) - 1); $('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1); } return false; }); // Setup status buttons $(document).on('click', '#imjs-status-panel .imjs-button', function() { var status = this.id.split('-')[2]; $('#imjs-away-message-text, #imjs-away-message-text-arrow').animate({ opacity: (status == 'away' ? 'show' : 'hide'), height: (status == 'away' ? 'show' : 'hide') }, 50); $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled'); $(this).addClass('imjs-toggled'); if(self.current_status[0] == 'away') self._last_status_message = $('#imjs-away-message-text').val(); $('#imjs-away-message-text').val(status == 'away' ? self._last_status_message || AjaxIM.l10n.defaultAway : ''); self.status(status, $('#imjs-away-message-text').val()); return false; }); // Allow status message to be changed $(document) .on('keyup', '#imjs-away-message-text', (function() { var msg_type_timer = null; return function() { if(msg_type_timer) clearTimeout(msg_type_timer); msg_type_timer = setTimeout(function() { self._last_status_message = self.current_status[1] = $('#imjs-away-message-text') .addClass('imjs-loading').val(); self.status.apply(self, self.current_status); }, 250); }; })()); $(this).bind('changeStatusSuccessful changeStatusFailed', function() { $('#imjs-away-message-text').removeClass('imjs-loading'); }); // Setup reconnect button $(document).on('click', '#imjs-reconnect', function() { self.offline = false; store.remove(self.username + '-offline'); $('#imjs-reconnect').hide(); $('.imjs-input').attr('disabled', false); // Restore status to available $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled'); $('#imjs-button-available').addClass('imjs-toggled'); $(self.statuses).each(function() { $('#imjs-friends').removeClass('imjs-' + this); }); $('#imjs-friends').addClass('imjs-available'); $('#imjs-away-message-text, #imjs-away-message-text-arrow') .css('display', 'none'); // Set status self.current_status = ['available', '']; store.set(self.username + '-status', ['available', '']); self.status('available', ''); // Reconnect self.storage(); self.listen(); }); // Initialize the chatbox hash this.chats = {}; // On window resize, check scroller visibility $(window).resize(function() { try { self._scrollers(); } catch(e) {} }); // Set up event handling this.onEvent('hello', this.onHello); this.onEvent('message', this.onMessage); this.onEvent('status', this.onStatus); this.onEvent('notice', this.onNotice); this.onEvent('goodbye', this.onGoodbye); } else { return AjaxIM.init(options); } }; $.extend(AjaxIM.prototype, { // == Main == setup: function() { var self = this; $(this).trigger('loadComplete'); $('.imjs-scroll').css('display', 'none'); this.initTabBar(); this._scrollers(); this.username = store.get('user'); this._lastReconnect = 0; if(this.username && store.get(this.username + '-offline') == true) { this.offline = true; setTimeout(function() { self._showReconnect(); }, 0); return; } if(this.username) this.storage(); setTimeout(function() { if (!self.socket) self.listen(); }, 2000); }, // === {{{AjaxIM.}}}**{{{storage()}}}** === // // Retrieves chat session data from whatever storage engine is enabled // (provided that one is enabled at all). If a page reloads, this function // is called to restore the user's chat state (existing conversations, active tab). // This function is called //automatically//, upon initialization of the IM engine. storage: function() { var self = this, chatstore = store.get(this.username + '-chats'), friends = store.get(this.username + '-friends'), status = store.get(this.username + '-status') || ['available', '']; this.chatstore = chatstore || {}; this.friends = {}; this.current_status = status; if(friends) { $.each(friends, function(friend, data) { self.addFriend(friend, data.status, data.group); }); $('#imjs-friends').removeClass('imjs-not-connected') .addClass('imjs-' + status[0]); $('#imjs-button-' + status[0]).addClass('imjs-toggled'); if(status[0] == 'away') { setTimeout(function() { $('#imjs-away-message-text, #imjs-away-message-text-arrow').show(); }, 250); $('#imjs-away-message-text').val(this.current_status[1]); } } $.each(this.chatstore, function(username, convo) { if(!convo.length) return; var chatbox = self._createChatbox(username, true), msglog = chatbox.find('.imjs-msglog').empty(); chatbox.data('lastDateStamp', null).css('display', 'none'); if(typeof convo == 'string') convo = self.chatstore[username] = JSON.parse(convo); // Restore all messages, date stamps, and errors msglog.html(convo.join('')); $(self).trigger('chatRestored', [username, chatbox]); }); var activeTab = store.get(this.username + '-activeTab'); if(activeTab && activeTab in this.chats) { this.chats[activeTab].parents('.imjs-tab').click(); var msglog = this.chats[activeTab].find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } // Set username in Friends list var header = $('#imjs-friends-panel .imjs-header'); header.html(header.html().replace('{username}', this.username)); }, // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** === // // Clears all session data from the last known user. _clearSession: function() { var last_user = store.get('user'); $.each(['friends', 'activeTab', 'chats', 'status', 'connected'], function(i, key) { store.remove(last_user + '-' + key); }); store.set('user', ''); this.chats = {}; this.friends = {}; this.chatstore = {}; this.current_status = ['available', '']; $('.imjs-tab').not('.imjs-tab.imjs-default').remove(); $('.imjs-friend-group').not('.imjs-friend-group.imjs-default').remove(); delete this.username; }, // === {{{AjaxIM.}}}**{{{listen()}}}** === // // Queries the server for new messages. listen: function() { if(this.offline) return; var self = this; AjaxIM.get( this.actions.listen, {}, function(response) { if($.isArray(response)) { $.each(response, function(key, value) { self._parseMessage(value); }); } else if($.isPlainObject(response)) { self._parseMessage(response); } if (!self.socket) { setTimeout(function() { self.listen(); }, 0); } }, function(error) { self._notConnected(); $(self).trigger('pollFailed', ['not connected']); // Try reconnecting in n*2 seconds (max 16) self._reconnectIn = (self._lastReconnect < (new Date()) - 60000) ? 1000 : Math.min(self._reconnectIn * 2, 16000); self._lastReconnect = new Date(); if (!self.socket) { setTimeout(function() { self.listen(); }, self._reconnectIn); } }, this.actions.noop ); }, // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** === // _parseMessage: function(message) { this.triggerEvent(message); }, onHello: function(message) { var self = this; this._clearSession(); this.username = message.username; this.current_status = ['available', '']; store.set('user', message.username); store.set(this.username + '-status', this.current_status); $('#imjs-friends').attr('class', 'imjs-available'); $.each(message.friends, function() { var friend; if(this.length == 2) friend = this; else friend = [this.toString(), ['offline', '']]; self.addFriend(friend[0], friend[1], 'Friends'); }); store.set(this.username + '-friends', this.friends); // Set username in Friends list var header = $('#imjs-friends-panel .imjs-header'); header.html(header.html().replace('{username}', this.username)); // Set status available $('#imjs-away-message-text, #imjs-away-message-text-arrow').hide(); $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled'); $('#imjs-button-available').addClass('imjs-toggled'); }, onMessage: function(event) { this.incoming(event.from, event.body); }, onStatus: function(event) { this._friendUpdate(event.from, event.status, event.message); this._storeFriends(); }, onNotice: function(event) { }, onGoodbye: function(event) { this._notConnected(); }, // === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** === // // Handles a new message from another user. If a chatbox for that // user does not yet exist, one is created. If it does exist, but // is minimized, the user is notified but the chatbox is not brought // to the front. This function also stores the message, if a storage // method is set. // // ==== Parameters ==== // * {{{from}}} is the username of the sender. // * {{{message}}} is the body. incoming: function(from, message) { // check if IM exists, otherwise create new window // TODO: If friend is not on the buddylist, // should add them to a temp list? var chatbox = this._createChatbox(from), tab = chatbox.parents('.imjs-tab'); if(!$('#imjs-bar .imjs-selected').length) { tab.click(); } else if(tab.data('state') != 'active') { this.notification(tab); } this._store(from, this._addMessage('b', chatbox, from, message)); }, // === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** === // // Inserts a new friend into the friends list. If the group specified // doesn't exist, it is created. If the friend is already in this group, // they aren't added again, however, the friend item is returned. // // ==== Parameters ==== // * {{{username}}} is the username of the new friend. // * {{{status}}} is the current status of the friend. // * {{{group}}} is the user group to which the friend should be added. addFriend: function(username, status, group) { var group_id = 'imjs-group-' + md5.hex(group); if(!(group_item = $('#' + group_id)).length) { var group_item = $('.imjs-friend-group.imjs-default').clone() .removeClass('imjs-default') .attr('id', group_id) .data('group', group) .appendTo('#imjs-friends-list'); var group_header = group_item.find('.imjs-friend-group-header'); group_header.html(group_header.html().replace('{group}', group)); } var user_id = 'imjs-friend-' + md5.hex(username + group); if(!$('#' + user_id).length) { var user_item = group_item.find('ul li.imjs-default').clone() .removeClass('imjs-default') .addClass('imjs-' + status[0]) .attr('id', user_id) .data('friend', username) .appendTo(group_item.find('ul')); // if(status[0] == 'offline') // user_item.hide(); user_item.html( user_item.html() .replace('{username}', username) .replace('{status}', status[1]) ); user_item.find('.imjs-friend-status') .attr('title', status[1]); } this.friends[username] = {status: status, group: group}; this._updateFriendCount(); return this.friends[username]; }, // === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** === // // Counts the number of online friends and updates the friends count // in the friend tab. _updateFriendCount: function() { var friendsLength = 0; $.each(this.friends, function(u, f) { if(f.status[0] != 'offline') friendsLength++; }); $('#imjs-friends .imjs-tab-text span span').html(friendsLength); }, // === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** === // // If a storage method is enabled, the current state of the // user's friends list is stored. _storeFriends: function() { store.set(this.username + '-friends', this.friends); }, // === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** === // // Builds a chatbox based on the default chatbox HTML and CSS defined // in the current theme. Should a chatbox for this user already exist, // a new one is not created. Instead, it is either given focus (should // no other windows already have focus), or a notification is issued. // // As well, if the chatbox does not exist, an associated tab will be // created. // // ==== Parameters ==== // * {{{username}}} is the name of the user for whom the chatbox is intended // for. // * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox // upon creation. // // //Note:// New chatboxes are given an automatically generated ID in the // format of {{{#imjs-[md5 of username]}}}. _createChatbox: function(username, no_stamp) { var self = this, chatbox_id = 'imjs-' + md5.hex(username); if(!(chatbox = $('#' + chatbox_id)).length) { // add a tab var tab = this.addTab(username, '#' + chatbox_id); var chatbox = tab.find('.imjs-chatbox'); chatbox.attr('id', chatbox_id); // remove default items from the message log var message_log = chatbox.find('.imjs-msglog').empty(); // setup the chatbox header var cb_header = chatbox.find('.imjs-header'); cb_header.html(cb_header.html().replace('{username}', username)); if(!no_stamp) { // add a date stamp this._store(username, this._addDateStamp(chatbox)); } // associate the username with the object and vice-versa this.chats[username] = chatbox; chatbox.data('username', username); if(username in this.friends) { status = this.friends[username].status; tab.addClass('imjs-' + status); } setTimeout(function() { self._scrollers(); }, 0); } else if(chatbox.parents('.imjs-tab').data('state') == 'closed') { chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old'); var tab = chatbox.parents('.imjs-tab'); if(tab.css('display') == 'none') tab.css('display', '').removeClass('imjs-selected') .insertAfter('#imjs-scroll-left') .data('state', 'minimized'); if(!no_stamp) { // possibly add a date stamp this._store(username, this._addDateStamp(chatbox)); } if(!$('#imjs-bar .imjs-selected').length) { tab.click(); } else { this.notification(tab); } setTimeout(function() { self._scrollers() }, 0); } return chatbox; }, // === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** // // // Adds a date/time notifier to a chatbox. These are generally // inserted upon creation of a chatbox, or upon the date changing // since the last time a date stamp was added. If a date stamp for // the current date already exists, a new one will not be added. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{time}}} is the date/time the date stamp will show. It is specified // in milliseconds since the Unix Epoch. This is //only// defined when // date stamps are being restored from storage; if not specified, the // current computer time will be used. _addDateStamp: function(chatbox, time) { var message_log = $(chatbox).find('.imjs-msglog'); if(!time) time = (new Date()).getTime(); var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(dateFormat(time, date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date'); var formatted_date = dateFormat(time, date_stamp_date.html()); if(chatbox.data('lastDateStamp') != formatted_date) { if(date_stamp_date.length) date_stamp_date.html(dateFormat(time, date_stamp_date.html())); chatbox.data('lastDateStamp', formatted_date); date_stamp.appendTo(message_log); return { replace_last: false, html: jQuery('
').append(date_stamp.clone()).html() }; } else { //$('
').appendTo(message_log); return {replace_last: false, html: ''}; } }, // === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** // // // Adds an error to a chatbox. These are generally inserted after // a user sends a message unsuccessfully. If an error message // was already added, another one will be added anyway. // // ==== Parameters ==== // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{error}}} is the error message string. // * {{{time}}} is the date/time the error occurred. It is specified in // milliseconds since the Unix Epoch. This is //only// defined when // errors are being restored from storage; if not specified, the current // computer time will be used. _addError: function(chatbox, error, time) { var message_log = $(chatbox).find('.imjs-msglog'); var error_item = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone(); var error_item_time = error_item.find('.imjs-msg-time'); if(error_item_time.length) { if(!time) time = (new Date()).getTime(); error_item_time.html(dateFormat(time, error_item_time.html())); } error_item.find('.imjs-error-error').html(error); error_item.appendTo(message_log); message_log[0].scrollTop = message_log[0].scrollHeight; return { replace_last: false, html: jQuery('
').append(error_item.clone()).html() }; }, // === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** // // // Adds a message to a chatbox. Depending on the {{{ab}}} value, // the color of the username may change as a way of visually // identifying users (however, this depends on the theme's CSS). // A timestamp is added to the message, and the chatbox is scrolled // to the bottom, such that the new message is visible. // // Messages will be automatically tag-escaped, so as to prevent // any potential cross-site scripting problems. Additionally, // URLs will be automatically linked. // // ==== Parameters ==== // * {{{ab}}} refers to whether the user is "a" or "b" in a conversation. // For the general case, "you" are "a" and "they" are "b". // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element. // * {{{username}}} is the username of the user who sent the message. // * {{{time}}} is the time the message was sent in milliseconds since // the Unix Epoch. This is //only// defined when messages are being // restored from storage. For new messages, the current computer // time is automatically used. _addMessage: function(ab, chatbox, username, message, time) { var last_message = chatbox.find('.imjs-msglog > *:last-child'); if(last_message.hasClass('imjs-msg-' + ab)) { // Last message was from the same person, so let's just add another imjs-msg-*-msg var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container') ? last_message : last_message.find('.imjs-msg-' + ab + '-container')); var single_message = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg') .clone().appendTo(message_container); single_message.html(single_message.html().replace('{username}', username)); } else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) { var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab) .clone().appendTo(chatbox.find('.imjs-msglog')); message_group.html(message_group.html().replace('{username}', username)); var single_message = message_group.find('.imjs-msg-' + ab + '-msg'); } // clean up the message message = message.toString().replace(//g, '>') .replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1$2$3'); // autolink URLs message = message.replace( new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' + '(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' + '(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'), '$1'); // insert the message single_message.html(single_message.html().replace('{message}', message)); // set the message time var msgtime = single_message.find('.imjs-msg-time'); if(!time) time = new Date(); if(typeof time != 'string') time = dateFormat(time, msgtime.html()); msgtime.html(time); var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; return { replace_last : !!message_container, html: jQuery('
').append( message_container ? last_message.clone() : message_group.clone() ).html() }; }, _store: function(username, msg) { if(!msg.html.length) return; if(!this.chatstore) this.chatstore = {}; if(!(username in this.chatstore)) { this.chatstore[username] = []; } else if(this.chatstore[username].length > 300) { // If the chat store gets too long, it becomes slow to load. this.chatstore[username].shift(); } if(msg.replace_last) this.chatstore[username].pop(); this.chatstore[username].push(msg.html); store.set(this.username + '-chats', this.chatstore); }, // === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** === // // Called when a friend's status is updated. This function will update all locations // where a status icon is displayed (chat tab, friends list), as well as insert // a notification, should a chatbox be open. // // ==== Parameters ==== // * {{{friend}}} is the username of the friend. // * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available // codes. //Note: If an invalid status is specified, no action will be taken.// // * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be // used should "you" send the user an IM while they are away, or if their status is viewed // in another way (such as via the friends list [**not yet implemented**]). _friendUpdate: function(friend, status, statusMessage) { if(this.chats[friend]) { var tab = this.chats[friend].parents('.imjs-tab'); var tab_class = 'imjs-tab'; if(tab.data('state') == 'active') tab_class += ' imjs-selected'; tab_class += ' imjs-' + status; tab.attr('class', tab_class); // display the status in the chatbox var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone(); var date_stamp_time = date_stamp.find('.imjs-msg-time'); if(date_stamp_time.length) date_stamp_time.html(dateFormat(date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date').html( AjaxIM.l10n[ 'chat' + status.toUpperCase().slice(0, 1) + status.slice(1) ].replace(/%s/g, friend)); var msglog = this.chats[friend].find('.imjs-msglog'); date_stamp.appendTo(msglog); msglog[0].scrollTop = msglog[0].scrollHeight; } if (!this.friends[friend]) { this.addFriend(friend, [status, statusMessage], 'Friends'); } if(this.friends[friend]) { var friend_id = 'imjs-friend-' + md5.hex(friend + this.friends[friend].group); $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status); $('#' + friend_id).find('.imjs-friend-status') .html(statusMessage) .attr('status', statusMessage); if(status == 'offline') { $('#' + friend_id + ':visible').slideUp(); $('#' + friend_id + ':hidden').hide(); } else if(!$('#' + friend_id + ':visible').length) { $('#' + friend_id).slideDown(); } this.friends[friend].status = [status, statusMessage]; this._updateFriendCount(); } }, // === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** === // // Puts the user into a visible state of disconnection. Sets the // friends list to "not connected" and empties it; disallows new messages // to be sent. _notConnected: function() { $('#imjs-friends') .addClass('imjs-not-connected') .unbind('click', this.activateTab); if($('#imjs-friends').hasClass('imjs-selected')) this.activateTab($('#imjs-friends')); }, _showReconnect: function() { $('#imjs-reconnect').show(); }, // === {{{AjaxIM.}}}**{{{send(to, message)}}}** === // // Sends a message to another user. The message will be added // to the chatbox before it is actually sent, however, if an // error occurs during sending, that will be indicated immediately // afterward. // // After sending the message, one of three status codes should be // returned as a JSON object, e.g. {{{{r: 'code'}}}}: // * {{{ok}}} — Message was sent successfully. // * {{{offline}}} — The user is offline or unavailable to // receive messages. // * {{{error}}} — a problem occurred, unrelated to the user // being unavailable. // // ==== Parameters ==== // * {{{to}}} is the username of the recipient. // * {{{message}}} is the content to be sent. send: function(username, body) { if(!body) return; var self = this; if(this.chats[username]) { // possibly add a datestamp this._store(username, this._addDateStamp(this.chats[username])); this._store(username, this._addMessage('a', this.chats[username], this.username, body)); } $(this).trigger('sendingMessage', [username, body]); var event = {type: 'message', from: this.username, to: username, body: body}; this.sendEvent(event, function(result) { if(result._status.sent) { $(self).trigger('sendMessageSuccessful', [username, body]); } else if(result.type == 'error') { if(result.error == 'not online') $(self).trigger('sendMessageFailed', ['offline', username, body]); else $(self).trigger('sendMessageFailed', [result.error, username, body]); } }, function(error) { self._notConnected(); var error = self._addError( self.chats[username], 'You are currently not connected or the ' + 'server is not available. Please ensure ' + 'that you are signed in and try again.'); self._store(error); $(self).trigger('sendMessageFailed', ['not connected', username, body]); }); }, // === {{{AjaxIM.}}}**{{{status(s, message)}}}** === // // Sets the user's status and status message. It is possible to not // set a status message by setting it to an empty string. The status // will be sent to the server, where upon the server will broadcast // the update to all individuals with "you" on their friends list. // // ==== Parameters ==== // * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}. // * {{{message}}} is the custom status message. status: function(value, message) { var self = this; // update status icon(s) if(!~this.statuses.indexOf(value)) return; // check if selected before writing over the class! $(this.statuses).each(function() { $('#imjs-friends').removeClass('imjs-' + this); }); $('#imjs-friends').addClass('imjs-' + value); $(this).trigger('changingStatus', [value, message]); if(value == 'offline') { self._notConnected(); self._showReconnect(); store.set(this.username + '-offline', true); self.offline = true; $('.imjs-input').attr('disabled', true); AjaxIM.post( this.actions.signoff, {}, function(result) { if(result.type == 'success') $(self).trigger('changeStatusSuccessful', [value, null]); }, function(error) { $(self).trigger('changeStatusFailed', ['not connected', value, null]); } ); } else { var event = {type: 'status', status: value, message: message}; this.sendEvent(event, function(result) { if(result._status.send) { $(self).trigger('sendMessageSuccessful', [username, body]); } else if(result.type == 'error') { if(result.error == 'not online') $(self).trigger('sendMessageFailed', ['offline', username, body]); else $(self).trigger('sendMessageFailed', [result.error, username, body]); } }, function(error) { self._notConnected(); var error = self._addError( self.chats[username], 'You are currently not connected or the ' + 'server is not available. Please ensure ' + 'that you are signed in and try again.'); self._store(error); $(self).trigger('sendMessageFailed', ['not connected', username, body]); }); } }, // === {{{AjaxIM.}}}**{{{statuses}}}** === // // These are the available status codes and their associated identities: // * {{{offline}}} (0) — Only used when signing out/when another // user has signed out, as once this status is set, the user is removed // from the server and friends will be unable to contact the user. // * {{{available}}} (1) — The user is online and ready to be messaged. // * {{{away}}} (2) — The user is online but is not available. Others // may still contact this user, however, the user may not respond. Anyone // contacting an away user will receive a notice stating that the user is away, // and (if one is set) their custom status message. // * {{{invisible}}} (3; **not yet implemented**) — The user is online, // but other users are made unaware, and the user will be represented // as being offline. It is still possible to contact this user, and for this // user to contact others; no status message or notice will be sent to others // messaging this user. statuses: ['offline', 'available', 'away'], // === {{{AjaxIM.}}}**{{{initTabs()}}}** === // // Setup the footer bar and enable tab actions. This function // uses {{{jQuery.live}}} to set hooks on any bar tabs created // in the future. initTabBar: function() { var self = this; // Set up your standard tab actions $(document) .on('click', '.imjs-tab', function() { return self.activateTab.call(self, $(this)); }); $(document) .on('click', '.imjs-tab .imjs-close', function() { return self.closeTab.call(self, $(this)); }); // Set up the friends list actions $(document).click(function(e) { if(~['imjs-friends'].indexOf(e.target.id) || $(e.target).parents('#imjs-friends').length) { return; } if($('#imjs-friends').data('state') == 'active') self.activateTab.call(self, $('#imjs-friends')); else if($('#imjs-status').data('state') == 'active') self.activateTab.call(self, $('#imjs-status')); }); $('#imjs-friends') .data('state', 'minimized') .click(function(e) { if(!$(this).hasClass('imjs-not-connected') && e.target.id != 'imjs-friends-panel' && !$(e.target).parents('#imjs-friends-panel').length) self.activateTab.call(self, $(this)); }) .mouseenter(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip') .css('display', 'block') .find('p') .html(AjaxIM.l10n.notConnectedTip); var tip_left = $(this).offset().left - $('.imjs-tooltip').outerWidth() + ($(this).outerWidth() / 2); var tip_top = $(this).offset().top - $('.imjs-tooltip').outerHeight(true); $('.imjs-tooltip').css({ left: tip_left, top: tip_top }); } }) .mouseleave(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', ''); } }); $('#imjs-friends-panel').css('display', 'none'); }, // === {{{AjaxIM.}}}**{{{activateTab()}}}** === // // Activate a tab by setting it to the 'active' state and // showing any related chatbox. If a chatbox is available // for this tab, also focus the input box. // // //Note:// {{{this}}}, here, refers to the tab DOM element. activateTab: function(tab) { var chatbox = tab.find('.imjs-chatbox') || false, input; if(tab.data('state') != 'active') { if(tab.attr('id') != 'imjs-friends') { $('#imjs-bar > li') .not(tab) .not('#imjs-friends, .imjs-scroll, .imjs-default') .add(tab.attr('id') == 'imjs-status' ? '#imjs-friends' : '') .removeClass('imjs-selected') .each(function() { var self = $(this); if(self.data('state') != 'closed') { self.data('state', 'minimized'); var chatbox = self.find('.imjs-chatbox'); if(chatbox.length) chatbox.css('display', 'none'); } }); } else { $('#imjs-status') .removeClass('imjs-selected') .data('state', 'minimized') .find('.imjs-chatbox') .css('display', 'none'); } if(chatbox && chatbox.css('display') == 'none') chatbox.css('display', ''); // set the tab to active... tab.addClass('imjs-selected').data('state', 'active'); // ...and hide and reset the notification icon tab.find('.imjs-notification').css('display', 'none') .data('count', 0); if(chatbox && (username = chatbox.data('username'))) store.set(this.username + '-activeTab', username); $(this).trigger('tabToggled', ['activated', tab]); } else { tab.removeClass('imjs-selected').data('state', 'minimized'); if(chatbox && chatbox.css('display') != 'none') chatbox.css('display', 'none'); store.set(this.username + '-activeTab', ''); $(this).trigger('tabToggled', ['minimized', tab]); } if(chatbox) { if((input = chatbox.find('.imjs-input')).length && !input.data('height')) { input.height(0); if(input[0].scrollHeight > input.height() || input[0].scrollHeight < input.height()) { input.height(input[0].scrollHeight); } // store the height for resizing later if (!input.height()) { input.height(16); } input.data('height', input.height()); } try { var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } catch(e) {} try { chatbox.find('.imjs-input').focus(); } catch(e) {} } }, // === {{{AjaxIM.}}}**{{{closeTab()}}}** === // // Close a tab and hide any related chatbox, such that // the chatbox can not be reopened without reinitializing // the tab. // // //Note:// {{{this}}}, here, refers to the tab DOM element. closeTab: function(tab) { tab = tab.parents('.imjs-tab'); tab.css('display', 'none') .removeClass('imjs-selected') .data('state', 'closed'); delete this.chatstore[tab.find('.imjs-chatbox').data('username')]; store.set(this.username + '-chats', this.chatstore); $(this).trigger('tabToggled', ['closed', tab]); this._scrollers(); return false; }, // === {{{AjaxIM.}}}**{{{addTab(label, action, closable)}}}** === // // Adds a tab to the tab bar, with the label {{{label}}}. When // clicked, it will call a callback function, {{{action}}}. If // {{{action}}} is a string, it is assumed that the string is // referring to a chatbox ID. // // ==== Parameters ==== // * {{{label}}} is the text that will be displayed on the tab.\\ // * {{{action}}} is the callback function, if it is a non-chatbox // tab, or a string if it //is// a chatbox tab.\\ // * {{{closable}}} is a boolean value that determines whether or not // it is possible for a user to close this tab. // // //Note:// New tabs are given an automatically generated ID // in the format of {{{#imjs-tab-[md5 of label]}}}. addTab: function(label, action, closable) { var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-left'); tab.removeClass('imjs-default') .attr('id', 'imjs-tab-' + md5.hex(label)) .html(tab.html().replace('{label}', label)) .data('state', 'minimized'); var notification = tab.find('.imjs-notification'); notification.css('display', 'none') .data('count', 0) .data('default-text', notification.html()) .html(notification.html().replace('{count}', '0')); if(closable === false) tab.find('.imjs-close').eq(0).remove(); if(typeof action != 'string') { tab.find('.imjs-chatbox').remove(); tab.click(action); } return tab; }, // === {{{AjaxIM.}}}**{{{notification(tab)}}}** === // // Displays a notification on a tab. Generally, this is called when // a tab is minimized to let the user know that there is an update // for them. The way the notification is displayed depends on the // theme CSS. // // ==== Parameters ==== // * {{{tab}}} is the jQuery-selected tab DOM element. notification: function(tab) { var notify = tab.find('.imjs-notification'); var notify_count = notify.data('count') + 1; notify.data('count', notify_count) .html(notify.data('default-text').replace('{count}', notify_count)) .css('display', ''); }, // === //private// {{{AjaxIM.}}}**{{{_scrollers()}}}** === // // Document me! _scrollers: function() { var needScrollers = false; $('#imjs-scroll-left').nextAll('.imjs-tab') .filter(function() { return $(this).data('state') != 'closed'; }) .each(function(i, tab) { tab = $(tab).css('display', ''); var tab_pos = tab.position(); if(tab_pos.top >= $('#imjs-bar').height() || tab_pos.left < 0 || tab_pos.right > $(document).width()) { $('.imjs-scroll').css('display', ''); tab.css('display', 'none'); needScrollers = true; } }); if(!needScrollers) { $('.imjs-scroll').css('display', 'none'); } if($('#imjs-scroll-left').css('display') != 'none' && $('#imjs-scroll-right').position().top >= $('#imjs-bar').height()) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); } if($('#imjs-bar li.imjs-tab:visible').length) { while($('.imjs-selected').css('display') == 'none') $('#imjs-scroll-right').click(); } this._scrollerIndex(); }, _scrollerIndex: function() { var hiddenRight = $('#imjs-bar li.imjs-tab:visible').slice(-1) .nextAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').eq(0) .prevAll('#imjs-bar li.imjs-tab:hidden') .not('.imjs-default') .filter(function() { return $(this).data('state') != 'closed' }).length; $('#imjs-scroll-left').html(hiddenLeft); $('#imjs-scroll-right').html(hiddenRight); }, unconfirmedEvents: {}, eventId: 1, createEvent: function() { var event = {}; event.id = this.eventId++;; this.unconfirmedEvents[event.id] = evt; }, sendEvent: function(event, successFunc, failureFunc) { event.id = this.eventId++; var evt = $.extend({}, event); evt['_status'] = { successFunc: successFunc, failureFunc: failureFunc }; this.unconfirmedEvents[event.id] = evt; if (this.socket) { this.socket.emit('server', event); } else { var self = this; var url = null; switch (event.type) { case 'message': url = this.actions.send; break; case 'status': url = this.actions.status; break; case 'signoff': url = this.actions.signoff; break; default: break; } AjaxIM.post(url, event, function(result) { if (result) { for (var e=0; e < result.length; ++e) { self.dispatchEvent(events[e]); } } }, function(error) { if (self.unconfirmedEvents[event.id]) { event = self.unconfirmedEvents[event.id]; event['_status']['sent'] = false; self.dispatchEvent(event); } } ); } }, dispatchEvent: function(event) { if (event.id && this.unconfirmedEvents[event.id]) { event['_status'] = $.extend({}, this.unconfirmedEvents[event.id]['_status'], event['_status']); delete this.unconfirmedEvents[event.id]; if (event['_status']['sent']) { event['_status']['successFunc'](event); } else { event['_status']['failureFunc'](event); } } else { this.triggerEvent(event); } }, // poor man's Backbone.js Events eventHandlers: {}, /** * Add a callback to listen for an event type. */ onEvent: function(eventType, callback) { if (!this.eventHandlers[eventType]) { this.eventHandlers[eventType] = []; } this.eventHandlers[eventType].push(callback); }, /** * Trigger an event on all interested callbacks. */ triggerEvent: function(event) { if (this.eventHandlers[event.type]) { for (var e=0; e < this.eventHandlers[event.type].length; ++e) { this.eventHandlers[event.type][e].call(this, event); } } } }) // == Static functions and variables == // // The following functions and variables are available outside of an initialized // {{{AjaxIM}}} object. // === {{{AjaxIM.}}}**{{{client}}}** === // // Once {{{AjaxIM.init()}}} is called, this will be set to the active AjaxIM // object. Only one AjaxIM object instance can exist at a time. This variable // can and should be accessed directly. AjaxIM.client = null; // === {{{AjaxIM.}}}**{{{init(options, actions)}}}** === // // Initialize the AjaxIM client object and engine. Here, you can define your // options and actions as outlined at the top of this documentation. // // ==== Parameters ==== // * {{{options}}} is the hash of custom settings to initialize Ajax IM with. // * {{{actions}}} is the hash of any custom action URLs. AjaxIM.init = function(options, actions) { if(!AjaxIM.client) AjaxIM.client = new AjaxIM(options, actions); return AjaxIM.client; }; // === {{{AjaxIM.}}}**{{{request(url, data, successFunc, failureFunc)}}}** === // // Wrapper around {{{$.jsonp}}}, the JSON-P library for jQuery, and {{{$.ajax}}}, // jQuery's ajax library. Allows either function to be called, automatically, // depending on the request's URL array (see {{{AjaxIM.actions}}}). // // ==== Parameters ==== // {{{url}}} is the URL of the request. // {{{data}}} are any arguments that go along with the request. // {{{success}}} is a callback function called when a request has completed // without issue. // {{{_ignore_}}} is simply to provide compatability with {{{$.post}}}. // {{{failure}}} is a callback function called when a request hasn't not // completed successfully. AjaxIM.post = function(url, data, successFunc, failureFunc, urlnoop) { AjaxIM.request(url, 'POST', data, successFunc, failureFunc, urlnoop); }; AjaxIM.get = function(url, data, successFunc, failureFunc, urlnoop) { AjaxIM.request(url, 'GET', data, successFunc, failureFunc, urlnoop); }; AjaxIM.request = function(url, type, data, successFunc, failureFunc, noopurl) { var errorTypes = ['timeout', 'error', 'notmodified', 'parseerror']; if(typeof failureFunc != 'function') failureFunc = function(){}; var jsonp = (url.substring(0, 1) !== '/'); var success = false; data['sessionid'] = cookies.get('sessionid'); $.ajax({ url: url, data: data, dataType: jsonp? 'jsonp': 'json', type: type, cache: false, timeout: 299000 }).done(function(data) { success = true; _dbg(JSON.stringify(data)); successFunc(data); }).fail(function(jqXHR, textStatus) { _dbg(textStatus); failureFunc(textStatus); }); if (jsonp) { setTimeout(function() { var failfn = function() { if (!success) { var textStatus = 'error'; _dbg(textStatus); failureFunc(textStatus); } }; if (noopurl) { var noopfn = function() { var noopdone = false; var event = {type: 'noop'}; $.ajax({ url: noopurl, data: event, dataType: 'jsonp', type: type, cache: false, timeout: 299000 }).done(function(data) { noopdone = true; if (!success) { setTimeout(noopfn, 3000); } }).fail(function(jqXHR, textStatus) { // since JSONP, never called }); setTimeout(function() { if (!noopdone) { failfn(); } }, 3000); }; noopfn(); } else { failfn(); } }, 3000); } // This prevents Firefox from spinning indefinitely // while it waits for a response. /* if(url == 'jsonp' && $.browser.mozilla) { $.jsonp({ 'url': 'about:', timeout: 0 }); } */ }; // === {{{AjaxIM.}}}**{{{incoming(data)}}}** === // // Never call this directly. It is used as a connecting function between // client and server for Comet. // // //Note:// There are two {{{AjaxIM.incoming()}}} functions. This one is a // static function called outside of the initialized AjaxIM object; the other // is only called within the initalized AjaxIM object. AjaxIM.incoming = function(data) { if(!AjaxIM.client) return false; if(data.length) AjaxIM.client._parseMessages(data); }; AjaxIM.eventID = 1; // === {{{AjaxIM.}}}**{{{l10n}}}** === // // Text strings used by Ajax IM. Should you want to translate Ajax IM into // another language, merely change these strings. // // {{{%s}}} denotes text that will be automatically replaced when the string is // used. AjaxIM._ = function(str) { if(str in AjaxIM.l10n) return AjaxIM.l10n[str]; return str; }; AjaxIM.l10n = { dayNames: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], monthNames: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], chatOffline: '%s signed off.', chatAvailable: '%s became available.', chatAway: '%s went away.', notConnected: 'You are currently not connected or the server is not available. ' + 'Please ensure that you are signed in and try again.', notConnectedTip: 'You are currently not connected.', defaultAway: 'I\'m away.' }; AjaxIM.debug = true; function _dbg(msg) { if(AjaxIM.debug && window.console) console.log(msg); } function uid(n){ var chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', nn=''; for(var c=0; c < n; c++){ nn += chars.substr(0|Math.random() * chars.length, 1); } return nn; }