// = im.js = // // **Copyright © 2005 – 2010 Joshua Gross**\\ // //MIT Licensed// // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // This is the main library for Ajax IM. It encompasses the UI controls, // and connecting with the server. It does //not// handle registration or // account management. (function($, _instance) { AjaxIM = function(options, actions) { if(this instanceof AjaxIM) { var self = this; // === {{{ defaults }}} === // // These are the available settings for Ajax IM, and the associated // defaults: // // * {{{pollType}}} determines the way in which the client will talk to // the server. // ** {{{comet}}} will use http streaming, wherein a connection to the server will be held open indefinitely, and the server will push down new events (messages, status updates) as they occur. // ** {{{long}}} will hold open a connection with the server for as long as possible, or until a new event is received. Upon the server sending an event or closing the connection, the client will automatically—and immediately—reconnect. // ** {{{short}}} will open a connection, and the server (if this method is supported) will //immediately// provide a response as to whether or not there are any new events. Once a response is received, the client will wait 5 seconds, and then reconnect. // * {{{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). // * {{{cookieName}}} is the name of the session cookie used by the server. // If this is not set properly, the IM engine will not be able to automatically // reinitialize sessions. // * {{{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}}}. // * {{{storageMethod}}} defines the way in which data (chat sessions) are // temporarily stored client-side. By default, {{{"flash"}}} is used because // it is the most widely supported method. However, // [[http://eric.garside.name/docs.html?p=jstore#js-engines|other storage engines]] // are available, with their respective up- and down-sides // outlined, on the jStore website. // * {{{storeSession}}} (**not implemented**) sets the number of days to // retain stored chat session data before it should be deleted. // * {{{checkResume}}} is a flag that sets whether or not the client should // make a call to the server before resuming the session (such as on a page // reload). This will ensure that the session has not expired. If set to {{{false}}}, // a call to the server will not be made, and the session will be assumed to // be active. var defaults = { pollType: 'long', pollServer: './ajaxim.php', cookieName: 'ajaxim_session', theme: 'themes/default', storageMethod: 'auto', flashStorage: 'js/jStore.Flash.html', storeSession: 5, // number of days to store chat data (0 for current session only) checkResume: true }; // === {{{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({ login: this.settings.pollServer + '/login', logout: this.settings.pollServer + '/logout', register: this.settings.pollServer + '/register', poll: this.settings.pollServer + '/poll', send: this.settings.pollServer + '/send', status: this.settings.pollServer + '/status', resume: this.settings.pollServer + '/resume' }, actions); var httpRx = new RegExp('^http://', 'i'), slashslashRx = new RegExp('^//', 'i'), queryStrRx = new RegExp('[?](.+)$'), subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i'); $.each(this.actions, function(name, action) { if(name == 'poll' || name == 'send') { if(self.settings.pollType != 'comet') action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action]; } else { action = ['ajax', action]; if(subdomainRx.test(action[1])) { document.domain = '.' + window.location.host; } else if((http = httpRx.test(action[1])) || slashslashRx.test(action[1])) { if(!(new RegExp('//' + window.location.host + '/', 'i')).test(action[1])) { action[1] += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?'; action = ['jsonp', action[1]]; } } } self.actions[name] = action; }); // 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) { $('
').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip', function() { self.themeLoaded = true; setup.apply(self); } ); if(typeof document.createStyleSheet == 'function') document.createStyleSheet(this.settings.theme + '/theme.css'); else $('body').append(''); } else { this.themeLoaded = true; setup.apply(this); } // Client-side storage for keeping track of // conversation states, active tabs, etc. // The default is flash storage, however, other // options are available via the jStore library. if(this.settings.storageMethod) { this.storageBrowserKey = 'unknown'; this.storeKey = ''; $.each($.browser, function(k, v) { if(k == 'version') return true; else if(v == true) { self.storageBrowserKey = k; return false; } }); if(this.settings.storageMethod == 'auto') { $.each(['ie', 'html5', 'local', 'flash'], function() { if($.jStore.Availability[this]()) { self.settings.storageMethod = this; return false; } }); } $.extend($.jStore.defaults, { project: 'im.js', engine: this.settings.storageMethod, flash: this.settings.flashStorage }); this.storageReady = false; if(this.settings.storageMethod == 'flash') { $.jStore.ready(function(engine) { $.jStore.flashReady(function() { self.storageReady = true; setup.apply(self); }); }) } else { $.jStore.ready(function(engine) { self.storageReady = true; setup.apply(self); }); } $.jStore.load(); } else { this.storageReady = true; setup.apply(this); } // Allow a chatbox to me minimized $('.imjs-chatbox').live('click', function(e) { e.preventDefault(); return false; }); $('.imjs-chatbox .imjs-minimize').live('click', function() { $(this).parents('.imjs-chatbox').data('tab').click(); }); // Allow a chatbox to be closed $('.imjs-chatbox .imjs-close').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.data('tab') .data('state', 'closed').css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[chatbox.data('username')]; if(self.storageReady) { $.jStore.store( self.storageBrowserKey + '-' + self.username + '-chats', self.chatstore); } } }); // Setup message sending for all chatboxes $('.imjs-chatbox .imjs-input').live('keydown', function(event) { var obj = $(this); if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) { self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } }).live('keyup', function(event) { if(event.keyCode == 13) { if($.browser.msie && $.browser.version < 8) { var obj = $(this); self.send(obj.parents('.imjs-chatbox').data('username'), obj.val()); } var obj = $(this); obj.val(''); obj.height(obj.data('height')); } }).live('keypress', function(e) { var obj = $(this); if(!($.browser.msie && $.browser.opera)) obj.height(0); if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) { obj.height(this.scrollHeight); } }); $('.imjs-msglog').live('click', function() { var chatbox = $(this).parents('.imjs-chatbox'); chatbox.find('.imjs-input').focus(); }); // Create a chatbox when a buddylist item is clicked $('.imjs-friend').live('click', function() { var chatbox = self._createChatbox($(this).data('friend')); if(chatbox.data('tab').data('state') != 'active') chatbox.data('tab').click(); chatbox.find('.imjs-input').focus(); }); // Setup and hide the scrollers $('.imjs-scroll').css('display', 'none'); $('#imjs-scroll-left').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').slice(-1) .next('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .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-right').html(parseInt($('#imjs-scroll-right').html()) + 1); } return false; }); $('#imjs-scroll-right').live('click', function() { var hiddenTab = $('#imjs-bar li.imjs-tab:visible').eq(0) .prev('#imjs-bar li.imjs-tab:hidden') .filter(function() { return $(this).data('state') != 'closed' }) .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-left').html(parseInt($('#imjs-scroll-left').html()) + 1); } return false; }); // Initialize the chatbox hash this.chats = {}; // Try to resume any existing session this.resume(); $(window).resize(function() { self.bar._scrollers(); }); } else return AjaxIM.init(options); }; // We predefine all public functions here... // If they are called before everything (theme, storage engine) has loaded, // then they get put into a "prequeue" and run when everything *does* // finally load. // // This ensures that nothing loads without all of the principal components // being pre-loaded. If that were to occur (without this prequeue), things // would surely break. var prequeue = []; var empty = function() { var func = this; return function() { prequeue.push([func, arguments]) }; }; $.extend(AjaxIM.prototype, { settings: {}, friends: {}, chats: {}, storage: empty.apply('storage'), login: empty.apply('login'), logout: empty.apply('logout'), resume: empty.apply('resume'), form: empty.apply('form'), poll: empty.apply('poll'), incoming: empty.apply('incoming'), send: empty.apply('send'), status: empty.apply('status'), statuses: {}, bar: { initialize: empty.apply('bar.initialize'), activateTab: empty.apply('bar.activateTab'), closeTab: empty.apply('bar.closeTab'), addTab: empty.apply('bar.addTab'), notification: empty.apply('bar.notification'), _scrollers: empty.apply('bar._scrollers') } }); setup = (function() { var self = this; if(!this.storageReady || !this.themeLoaded) return; $(self).trigger('loadComplete'); $(window).resize(); $.extend(AjaxIM.prototype, { // == Cookies == // // The "cookies" functions can be used to set, get, and erase JSON-based cookies. // These functions are primarily used to manage and read the server-set cookie // that handles the user's chat session ID. cookies: { // === {{{AjaxIM.}}}**{{{cookies.set(name, value, days)}}}** === // // Sets a cookie, stringifying the JSON value upon storing it. // // ==== Parameters ==== // * {{{name}}} is the cookie name.\\ // * {{{value}}} is the cookie data that you would like to store.\\ // * {{{days}}} is the number of days that the cookie will be stored for. set: function(name, value, days) { if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "; expires=" + date.toGMTString(); } else var expires = ""; document.cookie = name + "=" + $.compactJSON(value) + expires + "; path=/"; }, // === {{{AjaxIM.}}}**{{{cookies.get(name)}}}** === // // Gets a cookie, decoding the JSON value before returning the data. // // ==== Parameters ==== // * {{{name}}} is the cookie name that you would like to retrieve. get: function(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) { var cval = decodeURIComponent(c.substring(nameEQ.length, c.length)); return $.secureEvalJSON(cval); } } return null; }, // === {{{AjaxIM.}}}**{{{cookies.erase(name)}}}** === // // Deletes a cookie. // // {{{name}}} is the existing cookie that you would like to delete. erase: function(name) { self.cookies.set(name, '', -1); } }, // == Main == // === {{{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() { if(!self.storeKey.length) return; try { var chatstore = $.jStore.store(self.storeKey + 'chats') || {}; } catch(e) { $.jStore.remove(self.storeKey + 'chats'); var chatstore = {}; } if(this.chatstore) { $.each(this.chatstore, function(username, convo) { if(username in chatstore) chatstore[username] = $.merge(chatstore[username], self.chatstore[username]); else chatstore[username] = self.chatstore[username]; }); this.chatstore = chatstore; $.jStore.store(self.storeKey + 'chats', chatstore); } else { this.chatstore = chatstore; } $.each(this.chatstore, function(username, convo) { if(!convo.length) return; var chatbox = self._createChatbox(username, true); chatbox.data('lastDateStamp', null).css('display', 'none'); // Remove the automatic date stamp chatbox.find('.imjs-msglog').empty(); // Restore all messages, date stamps, and errors $.each(convo, function() { switch(this[0]) { case 'error': self._addError(chatbox, decodeURIComponent(this[2]), this[3]); break; case 'datestamp': self._addDateStamp(chatbox, this[3]); break; case 'a': case 'b': self._addMessage(this[0], chatbox, this[1], decodeURIComponent(this[2]), this[3]); break; } }); $(self).trigger('chatRestored', [username, chatbox]); }); var activeTab = $.jStore.store(self.storeKey + 'activeTab') || []; if(activeTab.length && (activeTab = activeTab[0]) && activeTab in this.chats) { this.chats[activeTab].data('tab').click(); var msglog = this.chats[activeTab].find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; } }, // === {{{AjaxIM.}}}**{{{login(username, password)}}}** === // // Authenticates a user and initializes the IM engine. If the user is // already logged in, [s]he is logged out, the session is cleared, and // the new user is logged in. // // Returns the user's properly formatted username, session ID, and online // friends in JSON, if successful; e.g.:\\ // {{{ {u: 'username', s: 'longsessionid', f: [{u: 'friend', s: 1, g: 'group'}]} }}} // // If unsuccessful, {{{false}}} is returned. // // ==== Parameters ==== // * {{{username}}} is the user's username.\\ // * {{{password}}} is the user's password. This password will be MD5 hashed // before it is sent to the server. login: function(username, password) { if(!username) username = ''; if(!password) password = ''; if(this.username) return true; // Already logged in! // hash password before sending it to the server password = $.md5(password); // authenticate AjaxIM.request( this.actions.login, {'username': username, 'password': password}, function(auth) { if(auth.r == 'logged in') { self.username = ('u' in auth ? auth.u : username); if(self.settings.storageMethod) self.storeKey = [self.storageBrowserKey, self.username, ''].join('-'); var existing = self.cookies.get(self.settings.cookieName); self.storage(); // Begin the session $.each(auth.f, function() { self.friends[this.u] = {status: [this.s, ''], group: this.g}; }); self._session(self.friends); self._storeFriends(); $(self).trigger('loginSuccessful', [auth]); return auth; } else { $(self).trigger('loginError', [auth]); } return false; } ); }, // === {{{AjaxIM.}}}**{{{logout()}}}** === // // Logs the user out and removes his/her session cookie. As well, it // will close all existing chat windows, clear the storage engine, and // remove the IM bar. logout: function() { AjaxIM.request( this.actions.logout, {}, function(done) { if(done) { self.cookies.erase(self.settings.cookieName); self._clearSession(); $(self).trigger('logoutSuccessful'); return true; } else { $(self).trigger('logoutError'); return false; } } ); }, // === {{{AjaxIM.}}}**{{{resume()}}}** === // // Resumes an existing session based on a session ID stored in the // server-set cookie. This function is called //automatically// upon IM // engine (re-)initialization, so the user does not need to re-login // should a session already exist. resume: function() { var session = this.cookies.get(this.settings.cookieName); if(session && session.sid) { this.username = session.user; this.storeKey = [this.storageBrowserKey, this.username, ''].join('-'); var friends = $.jStore.store(this.storeKey + 'friends') || []; if(self.settings.checkResume) { AjaxIM.request( this.actions.resume, {}, function(response) { if(response.r == 'connected') { self._session(friends); self.storage(); } else { var username = this.username; self._clearSession(); $(self).trigger('sessionNotResumed', [username]); } } ); } else { self._session(friends); self.storage(); } } else { $(self).trigger('noSession'); } }, // === //private// {{{AjaxIM.}}}**{{{_session(friends)}}}** === // // Restores session data (username, friends) and begins polling the server. // Called only by {{{AjaxIM.resume()}}}. // // ==== Parameters ==== // * {{{friends}}} is a list of "friend" objects, e.g.:\\ // {{{[{u: 'friend', s: 1, g: 'group'}, ...]}}} // ** {{{u}}} being the friend's username. // ** {{{s}}} being one of the available status codes (see {{{AjaxIM.statuses}}}), depending on the friend's current status. // ** {{{g}}} being the group that the friend is in. _session: function(friends) { $('#imjs-friends-panel .imjs-header span').html(this.username); $('#imjs-friends').removeClass('imjs-not-connected'); $.each(friends, function(friend, info) { self.addFriend.apply(self, [friend, info.status, info.group]); }); self._storeFriends(); $(self).trigger('sessionResumed', [this.username]); setTimeout(function() { self.poll(); }, 0); }, // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** === // // Clears all session data from the last known user. _clearSession: function() { if(self.settings.storageMethod && self.storageReady) { $.jStore.remove(self.storeKey + 'chats'); $.jStore.remove(self.storeKey + 'friends'); $.jStore.remove(self.storeKey + 'activeTab'); } self.chats = {}; $('.imjs-tab').not('.imjs-tab.imjs-default').remove(); self.cookies.erase('ajaxim_session'); delete self.username; }, // === {{{AjaxIM.}}}**{{{form(element)}}}** === // // Loads a login and registration form into the specified element // or, if no element is supplied, to the location on the page from // which this function was called. form: function(element) { $(element).load(this.settings.theme + '/theme.html #imjs-lr', function() { $('#imjs-lr .error').hide(); if(self.username) { $('#imjs-register, #imjs-login fieldset').hide(); $('#imjs-logged-in') .show() .html($('#imjs-logged-in').html().replace('{username}', self.username)); } else { $('#imjs-logged-in').hide(); } // Handle logout success $(self).bind('logoutSuccessful', function() { $('#imjs-login fieldset').slideDown(); $('#imjs-register').slideDown(); $('#imjs-logged-in') .html($('#imjs-logged-in strong').html('{username}')) .slideUp(); }); $('#imjs-logged-in a').click(function() { self.logout(); return false; }); // Handle login error or success $(self).bind('loginError', function() { $('#imjs-login .error').html(AjaxIM.i18n.authInvalid).slideDown('fast'); $('#imjs-login input') .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); }).bind('loginSuccessful', function() { $('#imjs-login fieldset').slideUp(); $('#imjs-register').slideUp(); $('#imjs-logged-in') .html($('#imjs-logged-in').html().replace('{username}', self.username)) .slideDown(); }); var login = function() { self.login($('#imjs-login-username').val(), $('#imjs-login-password').val()); return false; }; $('#imjs-login').submit(login); $('#imjs-login-submit').click(login); var regIssues = false; var regError = function(error, fields) { $('#imjs-register .error') .append(AjaxIM.i18n['register' + error] + ' ') .slideDown(); $(fields) .addClass('imjs-lr-error') .blur(function() { $(this).removeClass('imjs-lr-error'); }); regIssues = true; }; var register = function() { $('#imjs-register .error').empty(); regIssues = false; var username = $('#imjs-register-username').val(); var password = $('#imjs-register-password').val(); if(password.length < 4) regError('PasswordLength', '#imjs-register-password'); if(password != $('#imjs-register-cpassword').val()) regError('PasswordMatch', '#imjs-register-password, #imjs-register-cpassword'); if(username.length <= 2 || !$('#imjs-register-username').val().match(/^[A-Za-z0-9_.]+$/)) regError('UsernameLength', '#imjs-register-username'); if(!regIssues) { AjaxIM.request( self.actions.register, {username: username, password: password}, function(response) { if(response.r == 'registered') { self.login(username, password); } else if(response.r == 'error') { switch(response.e) { case 'unknown': regError('Unknown', ''); break; case 'invalid password': regError('PasswordLength', '#imjs-register-password'); break; case 'invalid username': regError('UsernameLength', '#imjs-register-username'); break; case 'username taken': regError('UsernameTaken', '#imjs-register-username'); break; } } }, function(error) { regError('Unknown', ''); } ); } return false; }; $('#imjs-register').submit(register); $('#imjs-register-submit').click(register); }); }, // === {{{AjaxIM.}}}**{{{poll()}}}** === // // Queries the server for new messages. If a 'long' or 'short' poll // type is used, jQuery's {{{$.post}}} or {{{$.getJSON}}} will be // used. If 'comet' is used, the server connection will be deferred // to the comet set of functions. poll: function() { if(/^(short|long)$/.test(this.settings.pollType)) { AjaxIM.request( this.actions.poll, {}, function(response) { if(!response['e']) { if(response.length) self._parseMessages(response); setTimeout(function() { self.poll(); }, 0); } else { switch(response.e) { case 'no session found': self._notConnected(); break; } $(self).trigger('pollFailed', [response.e]); } }, function(error) { self._notConnected(); $(self).trigger('pollFailed', ['not connected']); // try reconnecting? } ); } else if(this.settings.pollType == 'comet') { this.comet.connect(); } }, // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** === // // Handles an incoming message array:\\ // {{{[{t: 'type', s: 'sender', r: 'recipient', m: 'message'}, ...]}}} // // * {{{t}}} (message type) is one of: // ** {{{m}}} — a standard message // ** {{{s}}} — a user's status update // ** {{{b}}} — a broadcasted message (sent to many users simultaneously) // * {{{s}}} is the sender of the message. // * {{{r}}} is the intended recipient of the message. Most of the time, this will // simply be the logged in user, however, a broadcasted message may not specify // a recipient or may specify a different recipient. Also provides future // compatability for chatrooms. // * {{{m}}} is the actual message. For something such as a status update, this can // be a JSON object or parsable string; e.g. {{{"2:I'm away."}}} // // ==== Parameters ==== // * {{{messages}}} is the message array _parseMessages: function(messages) { if($.isArray(messages)) { $.each(messages, function() { $(self).trigger('parseMessage', [this]); switch(this.t) { case 'm': self.incoming(this.s, this.m); break; case 's': var status = this.m.split(':'); if(this['g']) self.addFriend(this.s, status, this.g); self._friendUpdate(this.s, status[0], status.slice(1).join(':')); self._storeFriends(); break; case 'b': break; default: break; } }); } }, // === {{{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); if(!$('#imjs-bar .imjs-selected').length) { chatbox.data('tab').click(); } else if(chatbox.data('tab').data('state') != 'active') { this.bar.notification(chatbox.data('tab')); } var time = this._addMessage('b', chatbox, from, message); this._storeMessage('b', chatbox, from, message, time); }, // === {{{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 status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); var group_id = 'imjs-group-' + $.md5(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(username + group); if(!$('#' + user_id).length) { var user_item = group_item.find('ul li.imjs-default').clone() .removeClass('imjs-default') .addClass('imjs-' + status_name) .attr('id', user_id) .data('friend', username) .appendTo(group_item.find('ul')); if(status[0] == 0) user_item.hide(); user_item.html(user_item.html().replace('{username}', username)); } 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; for(var f in this.friends) { if(this.friends[f].status[0] != 0) 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() { if(this.settings.storageMethod && this.storageReady) $.jStore.store(this.storeKey + '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 chatbox_id = 'imjs-' + $.md5(username); if(!(chatbox = $('#' + chatbox_id)).size()) { // add a tab var tab = this.bar.addTab(username, '#' + chatbox_id); var chatbox = tab.find('.imjs-chatbox'); chatbox.attr('id', chatbox_id); chatbox.data('tab', tab); // 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 var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } // associate the username with the object and vice-versa this.chats[username] = chatbox; chatbox.data('username', username); // did this chatbox fall down? this.bar._scrollers(); if(username in this.friends) { status = this.friends[username].status; var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status[0] == val) { status_name = key; return false; } }); tab.addClass('imjs-' + status_name); } // store inputbox height //var input = chatbox.find('.imjs-input'); //input.data('height', input.height()); } else if(chatbox.data('tab').data('state') == 'closed') { chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old'); var tab = chatbox.data('tab'); if(tab.css('display') == 'none') tab.css('display', '').removeClass('imjs-selected') .appendTo('#imjs-bar'); if(!no_stamp) { // possibly add a date stamp var time = this._addDateStamp(chatbox); this._storeNonMessage('datestamp', username, null, time); } if(!$('#imjs-bar .imjs-selected').length) { tab.click(); } else { this.bar.notification(tab); } } 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(AjaxIM.dateFormat(time, date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date'); var formatted_date = AjaxIM.dateFormat(time, date_stamp_date.html()); if(chatbox.data('lastDateStamp') != formatted_date) { if(date_stamp_date.length) date_stamp_date.html(AjaxIM.dateFormat(time, date_stamp_date.html())); chatbox.data('lastDateStamp', formatted_date); date_stamp.appendTo(message_log); } else { //$('
').appendTo(message_log); } return time; }, // === //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(AjaxIM.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; }, // === //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.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 = AjaxIM.dateFormat(time, msgtime.html()); msgtime.html(time); var msglog = chatbox.find('.imjs-msglog'); msglog[0].scrollTop = msglog[0].scrollHeight; return time; }, // === //private// {{{AjaxIM.}}}**{{{_storeNonMessage(type, username, data, time)}}}** === // // **Redundant?**\\ // Similar to {{{AjaxIM._storeMessage}}}, but stores items that aren't messages, // such as datestamps and errors. // // ==== Parameters ==== // * {{{type}}} is the type of non-message (error, datestamp). // * {{{username}}} is the username of the user being chatted with. // * {{{data}}} is the (optional) data of the non-message to be stored. // * {{{time}}} is the time of the message. _storeNonMessage: function(type, username, data, time) { // If storage is on & ready, store the non-message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; if(!(username in this.chatstore)) this.chatstore[username] = []; // If the chat store gets too long, it becomes slow to load. if(this.chatstore[username].length > 300) this.chatstore[username].shift(); // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[username].push( [type, username, encodeURIComponent(data), time]); if(this.storageReady) $.jStore.store(self.storeKey + 'chats', this.chatstore); } }, // === //private// {{{AjaxIM.}}}**{{{_storeMessage(ab, chatbox, username, message, time)}}}** === // // Taking the same arguments as {{{AjaxIM._addMessage}}}, {{{_storeMessage}}} pushes a message // into the storage hash, if storage is enabled. // // Messages are added to a message array, by username. The entire chat hash is stored as // a {{{'chats'}}} object in whatever storage engine is enabled. _storeMessage: function(ab, chatbox, username, message, time) { // If storage is on & ready, store the message if(this.settings.storageMethod) { if(!this.chatstore) this.chatstore = {}; message = message.replace(//g, '>'); 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(); } // For some reason, if we don't encode and decode the message, it *will* break // (at least) the Flash storage engine's retrieval. Gah! this.chatstore[chatbox.data('username')].push( [ab, username, encodeURIComponent(message), time]); if(this.storageReady) $.jStore.store(this.storeKey + '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) { // add friend to buddylist, update their status, etc. var status_name = 'available'; $.each(this.statuses, function(key, val) { if(status == val) { status_name = key; return false; } }); if(this.chats[friend]) { var tab = this.chats[friend].data('tab'); var tab_class = 'imjs-tab'; if(tab.data('state') == 'active') tab_class += ' imjs-selected'; tab_class += ' imjs-' + status_name; 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(AjaxIM.dateFormat(date_stamp_time.html())); var date_stamp_date = date_stamp.find('.imjs-date-date').html( AjaxIM.i18n[ 'chat' + status_name[0].toUpperCase() + status_name.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]) { var friend_id = 'imjs-friend-' + $.md5(friend + this.friends[friend].group); $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status_name); if(status == 0) { $('#' + 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); }, // === {{{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(to, message) { if(!message) return; if(this.chats[to]) { // REMOVE ME? // possibly add a datestamp var time = self._addDateStamp(this.chats[to]); this._storeNonMessage('datestamp', to, null, time); time = this._addMessage('a', this.chats[to], this.username, message); this._storeMessage('a', this.chats[to], this.username, message, time); } $(self).trigger('sendingMessage', [to, message]); AjaxIM.request( this.actions.send, {'to': to, 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('sendMessageSuccessful', [to, message]); break; case 'offline': $(self).trigger('sendMessageFailed', ['offline', to, message]); break; case 'error': default: if(result.e == 'no session found') { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); } $(self).trigger('sendMessageFailed', [result.e, to, message]); break; } }, function(error) { self._notConnected(); self._addError(self.chats[to], AjaxIM.i18n.notConnected); self._storeNonMessage('error', to, AjaxIM.i18n.notConnected, (new Date()).getTime()); $(self).trigger('sendMessageFailed', ['not connected', to, message]); } ); }, // === {{{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(s, message) { // update status icon(s) if(!this.statuses[s]) return; $('#imjs-friends').attr('class', 'imjs-' + s); $(self).trigger('changingStatus', [s, message]); AjaxIM.request( this.actions.status, {'status': this.statuses[s], 'message': message}, function(result) { switch(result.r) { case 'ok': $(self).trigger('changeStatusSuccessful', [s, message]); break; case 'error': default: $(self).trigger('changeStatusFailed', [result.e, s, message]); break; } }, function(error) { $(self).trigger('changeStatusFailed', ['not connected', s, message]); } ); }, // === {{{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: 0, available: 1, away: 2, invisible: 3}, // == Footer bar == // // The footer bar is the bar that sits at the bottom of the page, in a fixed // position. It contains a tab for the friends list, and tabs for any open // chat boxes. It is also possible to add custom tabs for other functionality. bar: { // === {{{AjaxIM.}}}**{{{bar.initialize()}}}** === // // Setup the footer bar and enable tab actions. This function // uses {{{jQuery.live}}} to set hooks on any bar tabs created // in the future. initialize: function() { // Set up your standard tab actions $('.imjs-tab') .live('click', this.activateTab); $('.imjs-tab .imjs-close') .live('click', this.closeTab); // Set up the friends list actions var self = this; $(document).click(function(e) { if(e.target.id == 'imjs-friends' || $(e.target).parents('#imjs-friends').length) { return; } if($('#imjs-friends').data('state') == 'active') self.activateTab.call($('#imjs-friends')); }); $('#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(this); }) .mouseenter(function() { if($(this).hasClass('imjs-not-connected')) { $('.imjs-tooltip').css('display', 'block'); $('.imjs-tooltip p').html(AjaxIM.i18n.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') .data('tab', $('#imjs-friends')) .css('display', 'none'); }, // === {{{AjaxIM.}}}**{{{bar.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() { var chatbox = $(this).find('.imjs-chatbox') || false; if($(this).data('state') != 'active') { if($(this).attr('id') != 'imjs-friends') { $('#imjs-bar > li') .not($(this)) .not('#imjs-friends') .removeClass('imjs-selected') .each(function() { if($(this).data('state') != 'closed') { $(this).data('state', 'minimized'); var chatbox = $(this).find('.imjs-chatbox'); if(chatbox.length) chatbox.css('display', 'none'); } }); } if(chatbox && chatbox.css('display') == 'none') chatbox.css('display', ''); // set the tab to active... var tab = $(this).addClass('imjs-selected').data('state', 'active'); // ...and hide and reset the notification icon tab.find('.imjs-notification').css('display', 'none') .data('count', 0); if(self.settings.storageMethod && self.storageReady && chatbox && (username = chatbox.data('username'))) { $.jStore.store(self.storeKey + 'activeTab', [username]); } $(self).trigger('tabToggled', ['activated', tab]); } else { var tab = $(this).removeClass('imjs-selected').data('state', 'minimized'); if(chatbox && chatbox.css('display') != 'none') chatbox.css('display', 'none'); if(self.settings.storageMethod && self.storageReady) { $.jStore.store(self.storeKey + 'activeTab', ['*']); } $(self).trigger('tabToggled', ['minimized', tab]); } if(chatbox) { if(!(input = chatbox.find('.imjs-input')).data('height')) { // store the height for resizing later 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.}}}**{{{bar.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() { var tab = $(this).parents('.imjs-tab'); tab.css('display', 'none').data('state', 'closed'); if(self.settings.storageMethod && self.storageReady) { delete self.chatstore[tab.find('.imjs-chatbox').data('username')]; if(self.storageReady) $.jStore.store(self.storeKey + 'chats', self.chatstore); } $(self).trigger('tabToggled', ['closed', tab]); self.bar._scrollers(); return false; }, // === {{{AjaxIM.}}}**{{{bar.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-right'); tab.removeClass('imjs-default') .attr('id', 'imjs-tab-' + $.md5(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.data('chatbox', action); } else { tab.find('.imjs-chatbox').remove(); tab.click(action); } return tab; }, // === {{{AjaxIM.}}}**{{{bar.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.}}}**{{{bar._scrollers()}}}** === // // Document me! _scrollers: function() { var needScrollers = false; $('.imjs-tab').filter(function() { return $(this).data('state') != 'closed' }).css('display', ''); $.each(self.chats, function(username, chatbox) { var tab = chatbox.data('tab'); if(tab.data('state') == 'closed') return true; if(tab.position().top > $('#imjs-bar').height()) { $('.imjs-scroll').css('display', ''); tab.css('display', 'none'); needScrollers = true; } else { tab.css('display', ''); } }); if(!needScrollers) { $('.imjs-scroll').css('display', 'none'); } if($('#imjs-scroll-left').css('display') != 'none' && $('#imjs-scroll-left').position().top > $('#imjs-bar').height()) { $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none'); } var hiddenLeft = $('#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 hiddenRight = $('#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); } }, // == Comet == // // Comet, or HTTP streaming, holds open a connection between the client and // the server indefinitely. As the server receives new messages or events, // they are passed down to the client in a {{{<script>}}} // tag which calls the {{{AjaxIM.incoming()}}} function. The connection is // opened using either an {{{iframe}}} (in Opera or Internet Explorer) or // an {{{XMLHTTPRequest}}} object. Due to the extra flexibility necessary // with the {{{XMLHTTPRequest}}} object, jQuery's {{{$.ajax}}} is not used. comet: { type: '', obj: null, onbeforeunload: null, onreadystatechange: null, iframe: function() { if(/loaded|complete/i.test(this.obj.readyState)) throw new Error("IM server not available!"); }, // === {{{AjaxIM.}}}**{{{comet.connect()}}}** === // // Creates and initializes the object and connection between the server // and the client. For Internet Explorer and Opera, we use an // {{{<iframe>}}} element; for all other browsers, we create an // {{{XMLHTTPRequest}}} object. The server connects to the URI defined // as the "poll" action. This function is called automatically, when // the IM engine is initialized and the {{{AjaxIM.poll()}}} function // is called. connect: function() { var self = _instance; if($.browser.opera || $.browser.msie) { var iframe = $(''); with(iframe) { css({ position: 'absolute', visibility: 'visible', display: 'block', left: '-10000px', top: '-10000px', width: '1px', height: '1px' }); attr('src', self.actions.poll[1]); appendTo('body'); bind('readystatechange', self.comet.onreadystatechange = function() { self.comet.iframe() }); bind('beforeunload', self.comet.onbeforeunload = function() { self.comet.disconnect() }); } self.comet.type = 'iframe'; self.comet.obj = iframe; } else { var xhr = new XMLHttpRequest; var length = 1029; var code = /^\s*]*>parent\.(.+);<\/script>$/; xhr.open('get', self.actions.poll[1], true); xhr.onreadystatechange = function(){ if(xhr.readyState > 2) { if(xhr.status == 200) { responseText = xhr.responseText.substring(length); length = xhr.responseText.length; if(responseText != ' ') eval(responseText.replace(code, "$1")); } // We need an "else" here. If the state changes to // "loaded", the user needs to know they're // disconnected. } }; self.comet.type = 'xhr'; self.comet.obj = xhr; addEventListener('beforeunload', self.comet.beforeunload = function() { self.comet.disconnect(); }, false); setTimeout(function() { xhr.send(null) }, 10); } }, // === {{{AjaxIM.}}}**{{{comet.disconnect()}}}** === // // Disconnect from the server and destroy the connection object. This // function is called before the page unloads, so that we plug up and // potential leaks and free memory. disconnect: function() { var self = _instance.comet; if(!this.type || !this.obj) return; if(this.type == 'iframe') { detachEvent('onreadystatechange', this.onreadystatechange); detachEvent('onbeforeunload', this.onbeforeunload); this.obj.src = '.'; $(this.obj).remove(); } else { removeEventListener('beforeunload', this.onbeforeunload, false); this.obj.onreadystatechange = function(){}; this.obj.abort(); } delete this.obj; } } }) self.bar.initialize(); if(prequeue.length) { $.each(prequeue, function() { var func = this[0].split('.'); if(func.length > 1) self[func[0]][func[1]].apply(self, this[1]); else self[func[0]].apply(self, this[1]); }); } }); // == 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(!_instance) { _instance = new AjaxIM(options, actions); AjaxIM.client = _instance; } return _instance; } // === {{{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.request = function(url, data, successFunc, failureFunc) { if(typeof failureFunc != 'function'); failureFunc = function(){}; $[url[0]]({ 'url': url[1], 'data': data, dataType: (url[0] == 'ajax' ? 'json' : 'jsonp'), type: 'POST', cache: false, timeout: 60000, callback: 'jsonp' + (new Date()).getTime(), success: function(json, textStatus) { successFunc(json); }, error: function(xOptions, error) { failureFunc(error); } }); // This prevents Firefox from spinning indefinitely // while it waits for a response. Why? Fuck if I know. if(url[0] == '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(!_instance) return false; if(data.length) _instance._parseMessages(data); } // === {{{AjaxIM.}}}**{{{loaded}}}** === // // If Ajax IM has been loaded with the im.load.js file, this function will be // called when the library is finally loaded and ready for use. Similar to // jQuery's $(document).ready(), but for Ajax IM. AjaxIM.loaded = function() { if(typeof AjaxIMLoadedFunction == 'function') { AjaxIMLoadedFunction(); delete AjaxIMLoadedFunction; // clean up the global namespace } }; // === {{{AjaxIM.}}}**{{{dateFormat([date,] [mask,] utc)}}}** === // // Date Format 1.2.3\\ // © 2007-2009 Steven Levithan ([[http://blog.stevenlevithan.com/archives/date-time-format|stevenlevithan.com]])\\ // MIT license // // Includes enhancements by Scott Trenda // and Kris Kowal ([[http://cixar.com/~kris.kowal/|cixar.com/~kris.kowal/]]) // // Accepts a date, a mask, or a date and a mask and returns a formatted version // of the given date. // // ==== Parameters ==== // * {{{date}}} is a {{{Date()}}} object. If not specified, the date defaults to the // current date/time. // * {{{mask}}} is a string that defines the formatting of the date. Formatting // options can be found in the // [[http://blog.stevenlevithan.com/archives/date-time-format|Date Format]] // documentation. If not specified, the mask defaults to {{{dateFormat.masks.default}}}. AjaxIM.dateFormat = function () { var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, timezone = new RegExp('\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) ' + '(?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b', 'g'), timezoneClip = /[^-+\dA-Z]/g, pad = function (val, len) { val = String(val); len = len || 2; while (val.length < len) val = "0" + val; return val; }; // Regexes and supporting functions are cached through closure return function (date, mask, utc) { var dF = AjaxIM.dateFormat; // You can't provide utc if you skip other args (use the "UTC:" mask prefix) if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { mask = date; date = undefined; } // Passing date through Date applies Date.parse, if necessary date = date ? new Date(date) : new Date; if (isNaN(date)) throw SyntaxError("invalid date"); mask = String(dF.masks[mask] || mask || dF.masks["default"]); // Allow setting the utc argument via the mask if (mask.slice(0, 4) == "UTC:") { mask = mask.slice(4); utc = true; } var _ = utc ? "getUTC" : "get", d = date[_ + "Date"](), D = date[_ + "Day"](), m = date[_ + "Month"](), y = date[_ + "FullYear"](), H = date[_ + "Hours"](), M = date[_ + "Minutes"](), s = date[_ + "Seconds"](), L = date[_ + "Milliseconds"](), o = utc ? 0 : date.getTimezoneOffset(), flags = { d: d, dd: pad(d), ddd: AjaxIM.i18n.dayNames[D], dddd: AjaxIM.i18n.dayNames[D + 7], m: m + 1, mm: pad(m + 1), mmm: AjaxIM.i18n.monthNames[m], mmmm: AjaxIM.i18n.monthNames[m + 12], yy: String(y).slice(2), yyyy: y, h: H % 12 || 12, hh: pad(H % 12 || 12), H: H, HH: pad(H), M: M, MM: pad(M), s: s, ss: pad(s), l: pad(L, 3), L: pad(L > 99 ? Math.round(L / 10) : L), t: H < 12 ? "a" : "p", tt: H < 12 ? "am" : "pm", T: H < 12 ? "A" : "P", TT: H < 12 ? "AM" : "PM", Z: utc ? "UTC" : (String(date).match(timezone) || [""]) .pop().replace(timezoneClip, ""), o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] }; return mask.replace(token, function ($0) { return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); }); }; }(); // Some common format strings AjaxIM.dateFormat.masks = { "default": "ddd mmm dd yyyy HH:MM:ss", shortDate: "m/d/yy", mediumDate: "mmm d, yyyy", longDate: "mmmm d, yyyy", fullDate: "dddd, mmmm d, yyyy", shortTime: "h:MM TT", mediumTime: "h:MM:ss TT", longTime: "h:MM:ss TT Z", isoDate: "yyyy-mm-dd", isoTime: "HH:MM:ss", isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" }; // === {{{AjaxIM.}}}**{{{i18n}}}** === // // 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.i18n = { 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.', authInvalid: 'Invalid username or password.', registerPasswordLength: 'Passwords must be more than 4 characters in length.', registerUsernameLength: 'Usernames must be more than 2 characters in length and ' + ' contain only A-Z, a-z, 0-9, underscores (_) and periods (.).', registerPasswordMatch: 'Entered passwords do not match.', registerUsernameTaken: 'The chosen username is already in use; please choose another.', registerUnknown: 'An unknown error occurred. Please try again.' } })(jQuery, false);