/*!
* Converse.js (XMPP-based instant messaging with Strophe.js and backbone.js)
* http://opkode.com
*
* Copyright (c) 2012 Jan-Carel Brand (jc@opkode.com)
* Dual licensed under the MIT and GPL Licenses
*/
/* The following line defines global variables defined elsewhere. */
/*globals jQuery, portal_url*/
// AMD/global registrations
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([
'Libraries/burry.js/burry',
"Libraries/jquery.gritter.min",
"Libraries/jquery.cookie",
"Libraries/jquery.ba-dotimeout.min",
"Libraries/underscore",
"Libraries/underscore.string/lib/underscore.string",
"Libraries/backbone",
"Libraries/strophe",
"Libraries/strophe.muc",
"Libraries/strophe.roster",
"Libraries/diff_match_patch",
"Libraries/jarnxmpp.core.handlers",
"Libraries/jarnxmpp.collaboration.protocol",
"Libraries/jarnxmpp.collaboration.collaborate"
], function (Burry) {
var store = new Burry.Store('collective.xmpp.chat');
// Use Mustache style syntax for variable interpolation
_.templateSettings = {
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
};
return factory(jarnxmpp, jQuery, store, _, console);
}
);
} else {
// Browser globals
var store = new Burry.Store('collective.xmpp.chat');
_.templateSettings = {
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
};
root.xmppchat = factory(jarnxmpp, jQuery, store, _, console || {log: function(){}});
}
}(this, function (jarnxmpp, $, store, _, console) {
var xmppchat = jarnxmpp;
xmppchat.messages = {};
xmppchat.messages.ClientStorage = (function () {
// TODO: Messages must be encrypted with a key and salt
methods = {};
methods.addMessage = function (jid, msg, direction) {
var bare_jid = Strophe.getBareJidFromJid(jid),
now = new Date().toISOString(),
msgs = store.get(bare_jid) || [];
if (msgs.length >= 30) {
msgs.shift();
}
msgs.push(now+' '+direction+' '+msg);
store.set(bare_jid, msgs);
};
methods.getMessages = function (jid) {
return store.get(jid) || [];
};
return methods;
})();
xmppchat.messages.getMessages = function (jid, callback) {
var bare_jid = Strophe.getBareJidFromJid(jid),
msgs = this.ClientStorage.getMessages(bare_jid);
callback(msgs);
};
xmppchat.collections = {
/* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc
* add-on for ejabberd wants the URL below. This might break for other
* Jabber servers.
*/
'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns'
};
xmppchat.collections.getLastCollection = function (jid, callback) {
var bare_jid = Strophe.getBareJidFromJid(jid),
iq = $iq({'type':'get'})
.c('list', {'xmlns': this.URI,
'with': bare_jid
})
.c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
.c('before').up()
.c('max')
.t('1');
xmppchat.connection.sendIQ(iq,
callback,
function () {
console.log('Error while retrieving collections');
});
};
xmppchat.collections.getLastMessages = function (jid, callback) {
var that = this;
this.getLastCollection(jid, function (result) {
// Retrieve the last page of a collection (max 30 elements).
var $collection = $(result).find('chat'),
jid = $collection.attr('with'),
start = $collection.attr('start'),
iq = $iq({'type':'get'})
.c('retrieve', {'start': start,
'xmlns': that.URI,
'with': jid
})
.c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
.c('max')
.t('30');
xmppchat.connection.sendIQ(iq, callback);
});
};
xmppchat.ChatBox = Backbone.Model.extend({
initialize: function () {
this.set({
'user_id' : Strophe.getNodeFromJid(this.get('jid')),
'box_id' : hex_sha1(this.get('jid')),
'fullname' : this.get('fullname')
});
}
});
xmppchat.ChatBoxView = Backbone.View.extend({
length: 200,
tagName: 'div',
className: 'chatbox',
events: {
'click .close-chatbox-button': 'closeChat',
'keypress textarea.chat-textarea': 'keyPressed'
},
message_template: _.template(
'
' +
'{{time}} {{username}}: ' +
'{{message}}' +
'
'),
action_template: _.template(
'
' +
'{{time}}: ' +
'{{message}}' +
'
'),
appendMessage: function (message) {
var time,
now = new Date(),
minutes = now.getMinutes().toString(),
list,
$chat_content,
match;
message = message.replace(//g,">").replace(/\"/g,""").replace(/^\s*/, "");
list = message.match(/\b(http:\/\/www\.\S+\.\w+|www\.\S+\.\w+|http:\/\/(?=[^w]){3}\S+[\.:]\S+)[^ ]+\b/g);
if (list) {
for (i = 0; i < list.length; i++) {
message = message.replace(list[i], ""+ list[i] + "" );
}
}
if (minutes.length==1) {minutes = '0'+minutes;}
time = now.toLocaleTimeString().substring(0,5);
$chat_content = $(this.el).find('.chat-content');
$chat_content.find('div.chat-event').remove();
match = message.match(/^\/(.*?)(?: (.*))?$/);
if ((match) && (match[1] === 'me')) {
message = message.replace(/^\/me/, '*'+xmppchat.username);
$chat_content.append(this.action_template({
'sender': 'me',
'time': time,
'message': message,
'username': xmppchat.username,
'extra_classes': ''
}));
} else {
$chat_content.append(this.message_template({
'sender': 'me',
'time': time,
'message': message,
'username': 'me',
'extra_classes': ''
}));
}
$chat_content.scrollTop($chat_content[0].scrollHeight);
},
insertStatusNotification: function (user_id, message) {
var $chat_content = this.$el.find('.chat-content');
$chat_content.find('div.chat-event').remove().end()
.append($('').text(user_id+' '+message));
$chat_content.scrollTop($chat_content[0].scrollHeight);
},
messageReceived: function (message) {
/* XXX: event.mtype should be 'xhtml' for XHTML-IM messages,
but I only seem to get 'text'.
*/
var body = $(message).children('body').text(),
jid = $(message).attr('from'),
composing = $(message).find('composing'),
$chat_content = $(this.el).find('.chat-content'),
user_id = Strophe.getNodeFromJid(jid),
delayed = $(message).find('delay').length > 0,
fullname = this.model.get('fullname'),
time, stamp;
if (xmppchat.xmppstatus.getOwnStatus() === 'offline') {
// only update the UI if the user is not offline
return;
}
if (!body) {
if (composing.length > 0) {
this.insertStatusNotification(fullname, 'is typing');
return;
}
} else {
xmppchat.messages.ClientStorage.addMessage(jid, body, 'from');
$chat_content.find('div.chat-event').remove();
if (delayed) {
// XXX: Test properly (for really old messages we somehow need to show
// their date as well)
stamp = $(message).find('delay').attr('stamp');
time = (new Date(stamp)).toLocaleTimeString().substring(0,5);
} else {
time = (new Date()).toLocaleTimeString().substring(0,5);
}
$chat_content.append(
this.message_template({
'sender': 'them',
'time': time,
'message': body.replace(/ /g, ""),
'username': fullname.split(' ')[0],
'extra_classes': delayed && 'delayed' || ''
}));
$chat_content.scrollTop($chat_content[0].scrollHeight);
}
},
insertClientStoredMessages: function () {
xmppchat.messages.getMessages(this.model.get('jid'), $.proxy(function (msgs) {
var $content = this.$el.find('.chat-content');
for (var i=0; i<_.size(msgs); i++) {
var msg = msgs[i],
msg_array = msg.split(' ', 2),
date = msg_array[0],
match;
msg = String(msg).replace(/(.*?\s.*?\s)/, '');
match = msg.match(/^\/(.*?)(?: (.*))?$/);
if (msg_array[1] == 'to') {
$content.append(
this.message_template({
'sender': 'me',
'time': new Date(Date.parse(date)).toLocaleTimeString().substring(0,5),
'message': msg,
'username': 'me',
'extra_classes': 'delayed'
}));
} else {
$content.append(
this.message_template({
'sender': 'them',
'time': new Date(Date.parse(date)).toLocaleTimeString().substring(0,5),
'message': msg,
'username': this.model.get('fullname').split(' ')[0],
'extra_classes': 'delayed'
}));
}
}
}, this));
},
sendMessage: function (text) {
// TODO: Also send message to all my own connected resources, so that
// they can display it as well....
// TODO: Look in ChatPartners to see what resources we have for the recipient.
// if we have one resource, we sent to only that resources, if we have multiple
// we send to the bare jid.
var bare_jid = this.model.get('jid');
var message = $msg({to: bare_jid, type: 'chat'})
.c('body').t(text).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
xmppchat.connection.send(message);
xmppchat.messages.ClientStorage.addMessage(bare_jid, text, 'to');
this.appendMessage(text);
},
keyPressed: function (ev) {
var $textarea = $(ev.target),
message,
notify,
composing,
that = this;
if(ev.keyCode == 13) {
message = $textarea.val();
$textarea.val('').focus();
if (message !== '') {
this.sendMessage(message);
}
$(this.el).data('composing', false);
} else {
composing = $(this.el).data('composing');
if (!composing) {
notify = $msg({'to':this.model.get('jid'), 'type': 'chat'})
.c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'});
xmppchat.connection.send(notify);
$(this.el).data('composing', true);
}
}
},
addChatToCookie: function () {
var cookie = jQuery.cookie('chats-open-'+xmppchat.username),
jid = this.model.get('jid'),
new_cookie,
open_chats = [];
if (cookie) {
open_chats = cookie.split('|');
}
if (!_.has(open_chats, jid)) {
// Update the cookie if this new chat is not yet in it.
open_chats.push(jid);
new_cookie = open_chats.join('|');
jQuery.cookie('chats-open-'+xmppchat.username, new_cookie, {path: '/'});
console.log('updated cookie = ' + new_cookie + '\n');
}
},
removeChatFromCookie: function () {
var cookie = jQuery.cookie('chats-open-'+xmppchat.username),
open_chats = [],
new_chats = [];
if (cookie) {
open_chats = cookie.split('|');
}
for (var i=0; i < open_chats.length; i++) {
if (open_chats[i] != this.model.get('jid')) {
new_chats.push(open_chats[i]);
}
}
if (new_chats.length) {
jQuery.cookie('chats-open-'+xmppchat.username, new_chats.join('|'), {path: '/'});
}
else {
jQuery.cookie('chats-open-'+xmppchat.username, null, {path: '/'});
}
},
closeChat: function () {
var that = this;
$('#'+this.model.get('box_id')).hide('fast', function () {
that.removeChatFromCookie(that.model.get('id'));
});
},
initialize: function (){
$('body').append($(this.el).hide());
xmppchat.roster.on('change', function (item, changed) {
if (_.has(changed.changes, 'presence_type')) {
if (this.$el.is(':visible')) {
if (item.get('presence_type') === 'offline') {
this.insertStatusNotification(this.model.get('fullname'), 'has gone offline');
} else if (item.get('presence_type') === 'away') {
this.insertStatusNotification(this.model.get('fullname'), 'has gone away');
} else if ((item.get('presence_type') === 'busy') || (item.get('presence_type') === 'dnd')) {
this.insertStatusNotification(this.model.get('fullname'), 'is busy');
} else if (item.get('presence_type') === 'online') {
this.$el.find('div.chat-event').remove();
}
}
} else if (_.has(changed.changes, 'status')) {
if (item.get('jid') === this.model.get('jid')) {
this.$el.find('p.user-custom-message').text(item.get('status'));
}
}
}, this);
},
template: _.template(
'
'),
initialize: function () {
var $select = $(this.el).find('select#select-xmpp-status'),
presence_type = this.model.getOwnStatus() || 'offline',
options = $('option', $select),
that = this;
$(this.el).html(this.choose_template());
$(this.el).find('#fancy-xmpp-status-select')
.html(this.status_template({
'chat_status': "I am " + presence_type,
'presence_type': presence_type
}));
// iterate through all the