2102 lines
94 KiB
JavaScript
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}}} — 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 {
|
|
//$('<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, '<').replace(/>/g, '>')
|
|
.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, '<').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 = $('<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\\
|
|
// © 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);
|