odoo/addons/web_chat/static/lib/AjaxIM/js/im.js

2102 lines
94 KiB
JavaScript

// = im.js =
//
// **Copyright © 2005 – 2010 Joshua Gross**\\
// //MIT Licensed//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This is the main library for Ajax IM. It encompasses the UI controls,
// and connecting with the server. It does //not// handle registration or
// account management.
(function($, _instance) {
AjaxIM = function(options, actions) {
if(this instanceof AjaxIM) {
var self = this;
// === {{{ defaults }}} ===
//
// These are the available settings for Ajax IM, and the associated
// defaults:
//
// * {{{pollType}}} determines the way in which the client will talk to
// the server.
// ** {{{comet}}} will use http streaming, wherein a connection to the server will be held open indefinitely, and the server will push down new events (messages, status updates) as they occur.
// ** {{{long}}} will hold open a connection with the server for as long as possible, or until a new event is received. Upon the server sending an event or closing the connection, the client will automatically—and immediately—reconnect.
// ** {{{short}}} will open a connection, and the server (if this method is supported) will //immediately// provide a response as to whether or not there are any new events. Once a response is received, the client will wait 5 seconds, and then reconnect.
// * {{{pollServer}}} is the default URL to which all actions refer. It is
// possible to specify certain action URLs separately (as is used with the
// NodeJS server).
// * {{{cookieName}}} is the name of the session cookie used by the server.
// If this is not set properly, the IM engine will not be able to automatically
// reinitialize sessions.
// * {{{theme}}} is the name of the theme folder that defines the HTML and
// CSS of the IM bar and chat boxes. Usually, themes are deposited in the
// provided "themes" folder and specified by that path, e.g. {{{themes/default}}}.
// Theme files within the theme folder must be named {{{theme.html}}} and
// {{{theme.css}}}.
// * {{{storageMethod}}} defines the way in which data (chat sessions) are
// temporarily stored client-side. By default, {{{"flash"}}} is used because
// it is the most widely supported method. However,
// [[http://eric.garside.name/docs.html?p=jstore#js-engines|other storage engines]]
// are available, with their respective up- and down-sides
// outlined, on the jStore website.
// * {{{storeSession}}} (**not implemented**) sets the number of days to
// retain stored chat session data before it should be deleted.
// * {{{checkResume}}} is a flag that sets whether or not the client should
// make a call to the server before resuming the session (such as on a page
// reload). This will ensure that the session has not expired. If set to {{{false}}},
// a call to the server will not be made, and the session will be assumed to
// be active.
var defaults = {
pollType: 'long',
pollServer: './ajaxim.php',
cookieName: 'ajaxim_session',
theme: 'themes/default',
storageMethod: 'auto',
flashStorage: 'js/jStore.Flash.html',
storeSession: 5, // number of days to store chat data (0 for current session only)
checkResume: true
};
// === {{{AjaxIM.}}}**{{{settings}}}** ===
//
// These are the settings for the IM. If particular options are not specified,
// the defaults (see above) will be used. //These options will be defined
// upon calling the initialization function, and not set directly.//
this.settings = $.extend(defaults, options);
// === {{{AjaxIM.}}}**{{{actions}}}** ===
//
// Each individual action that the IM engine can execute is predefined here.
// By default, it merely appends the action onto the end of the {{{pollServer}}} url,
// however, it is possible to define actions individually. //The alternative actions
// will be defined upon calling the initialization function, and not set directly.//
//
// Should you define an action at a different URL, Ajax IM will determine whether
// or not this URL is within the current domain. If it is within a subdomain of
// the current domain, it will set the document.domain variable for you,
// to match a broader hostname scope; the action will continue to use {{{$.post}}}
// (the default AJAX method for Ajax IM).
//
// On the other hand, should you choose a URL outside the current domain
// Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid
// cross-domain scripting issues. This means that a server on a different
// port or at a different address will need to be able to handle GET
// requests rather than POST requests (such as how the Node.JS Ajax IM
// server works).
this.actions = $.extend({
login: this.settings.pollServer + '/login',
logout: this.settings.pollServer + '/logout',
register: this.settings.pollServer + '/register',
poll: this.settings.pollServer + '/poll',
send: this.settings.pollServer + '/send',
status: this.settings.pollServer + '/status',
resume: this.settings.pollServer + '/resume'
}, actions);
var httpRx = new RegExp('^http://', 'i'),
slashslashRx = new RegExp('^//', 'i'),
queryStrRx = new RegExp('[?](.+)$'),
subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i');
$.each(this.actions, function(name, action) {
if(name == 'poll' || name == 'send') {
if(self.settings.pollType != 'comet')
action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?';
action = ['jsonp', action];
} else {
action = ['ajax', action];
if(subdomainRx.test(action[1])) {
document.domain = '.' + window.location.host;
} else if((http = httpRx.test(action[1])) || slashslashRx.test(action[1])) {
if(!(new RegExp('//' + window.location.host + '/', 'i')).test(action[1])) {
action[1] += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?';
action = ['jsonp', action[1]];
}
}
}
self.actions[name] = action;
});
// We load the theme dynamically based on the passed
// settings. If the theme is set to false, we assume
// that the user is going to load it himself.
this.themeLoaded = false;
if(this.settings.theme) {
$('<div></div>').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('<link rel="stylesheet" href="' +
this.settings.theme + '/theme.css" />');
} 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}}} &mdash; a standard message
// ** {{{s}}} &mdash; a user's status update
// ** {{{b}}} &mdash; 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 {
//$('<div></div>').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, '&lt;').replace(/>/g, '&gt;')
.replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1<strong>$2</strong>$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'),
'<a href="$1" target="_blank">$1</a>');
// 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, '&lt;').replace(/>/g, '&gt;');
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}}} &mdash; Message was sent successfully.
// * {{{offline}}} &mdash; The user is offline or unavailable to
// receive messages.
// * {{{error}}} &mdash; 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) &mdash; 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) &mdash; The user is online and ready to be messaged.
// * {{{away}}} (2) &mdash; 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**) &mdash; 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 {{{&lt;script&gt;}}}
// 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
// {{{&lt;iframe&gt;}}} 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 = $('<iframe></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*<script[^>]*>parent\.(.+);<\/script><br\s*\/>$/;
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\\
// &copy; 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);