From c2865075b16014ace644d6f0cb17e1ccc2cc4dcf Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 11 Apr 2011 11:26:46 +0200 Subject: [PATCH] web_chat im.js eol 0x0d to 0x0a bzr revid: al@openerp.com-20110411092646-dykbpwndmn5gawf1 --- addons/web_chat/static/lib/AjaxIM/js/im.js | 2102 +++++++++++++++++++- 1 file changed, 2101 insertions(+), 1 deletion(-) diff --git a/addons/web_chat/static/lib/AjaxIM/js/im.js b/addons/web_chat/static/lib/AjaxIM/js/im.js index ef80481a246..42c3f743204 100644 --- a/addons/web_chat/static/lib/AjaxIM/js/im.js +++ b/addons/web_chat/static/lib/AjaxIM/js/im.js @@ -1 +1,2101 @@ -// = 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?method=' + this.settings.pollType, 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); \ No newline at end of file +// = 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?method=' + this.settings.pollType, + 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') { + 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);