/*!
* 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 (console===undefined || console.log===undefined) {
console = { log: function () {}, error: function () {} };
}
if (typeof define === 'function' && define.amd) {
require.config({
paths: {
"burry": "Libraries/burry.js/burry",
"sjcl": "Libraries/sjcl",
"tinysort": "Libraries/jquery.tinysort",
"underscore": "Libraries/underscore",
"backbone": "Libraries/backbone",
"localstorage": "Libraries/backbone.localStorage",
"strophe": "Libraries/strophe",
"strophe.muc": "Libraries/strophe.muc",
"strophe.roster": "Libraries/strophe.roster",
"strophe.vcard": "Libraries/strophe.vcard"
},
// define module dependencies for modules not using define
shim: {
'backbone': {
//These script dependencies should be loaded before loading
//backbone.js
deps: [
'underscore',
'jquery'
],
//Once loaded, use the global 'Backbone' as the
//module value.
exports: 'Backbone'
},
'underscore': {
exports: '_'
},
'strophe.muc': {
deps: ['strophe', 'jquery']
},
'strophe.roster': {
deps: ['strophe', 'jquery']
},
'strophe.vcard': {
deps: ['strophe', 'jquery']
}
}
});
define("converse", [
"burry",
"localstorage",
"tinysort",
"sjcl",
"strophe.muc",
"strophe.roster",
"strophe.vcard"
], 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(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(jQuery, store, _, console || {log: function(){}});
}
}(this, function ($, store, _, console) {
var xmppchat = {};
xmppchat.msg_counter = 0;
var strinclude = function(str, needle){
if (needle === '') { return true; }
if (str === null) { return false; }
return String(str).indexOf(needle) !== -1;
};
xmppchat.autoLink = function (text) {
// Convert URLs into hyperlinks
var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g;
return text.replace(re, '$1');
};
xmppchat.toISOString = function (date) {
var pad;
if (typeof date.toISOString !== 'undefined') {
return date.toISOString();
} else {
// IE <= 8 Doesn't have toISOStringMethod
pad = function (num) {
return (num < 10) ? '0' + num : '' + num;
};
return date.getUTCFullYear() + '-' +
pad(date.getUTCMonth() + 1) + '-' +
pad(date.getUTCDate()) + 'T' +
pad(date.getUTCHours()) + ':' +
pad(date.getUTCMinutes()) + ':' +
pad(date.getUTCSeconds()) + '.000Z';
}
};
xmppchat.parseISO8601 = function (datestr) {
/* Parses string formatted as 2013-02-14T11:27:08.268Z to a Date obj.
*/
var numericKeys = [1, 4, 5, 6, 7, 10, 11],
struct = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}\.?\d*)Z\s*$/.exec(datestr),
minutesOffset = 0;
for (var i = 0, k; (k = numericKeys[i]); ++i) {
struct[k] = +struct[k] || 0;
}
// allow undefined days and months
struct[2] = (+struct[2] || 1) - 1;
struct[3] = +struct[3] || 1;
if (struct[8] !== 'Z' && struct[9] !== undefined) {
minutesOffset = struct[10] * 60 + struct[11];
if (struct[9] === '+') {
minutesOffset = 0 - minutesOffset;
}
}
return new Date(Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]));
};
xmppchat.updateMsgCounter = function () {
this.msg_counter += 1;
if (this.msg_counter > 0) {
if (document.title.search(/^Messages \(\d\) /) === -1) {
document.title = "Messages (" + this.msg_counter + ") " + document.title;
} else {
document.title = document.title.replace(/^Messages \(\d\) /, "Messages (" + this.msg_counter + ") ");
}
window.blur();
window.focus();
} else if (document.title.search(/^\(\d\) /) !== -1) {
document.title = document.title.replace(/^Messages \(\d\) /, "");
}
};
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.Message = Backbone.Model.extend();
xmppchat.Messages = Backbone.Collection.extend({
model: xmppchat.Message
});
xmppchat.ChatBox = Backbone.Model.extend({
initialize: function () {
if (this.get('box_id') !== 'controlbox') {
this.messages = new xmppchat.Messages();
this.messages.localStorage = new Backbone.LocalStorage(
hex_sha1('converse.messages'+this.get('jid')));
this.set({
'user_id' : Strophe.getNodeFromJid(this.get('jid')),
'box_id' : hex_sha1(this.get('jid')),
'fullname' : this.get('fullname'),
'url': this.get('url'),
'image_type': this.get('image_type'),
'image_src': this.get('image_src')
});
}
},
messageReceived: function (message) {
var $message = $(message),
body = xmppchat.autoLink($message.children('body').text()),
from = Strophe.getBareJidFromJid($message.attr('from')),
composing = $message.find('composing'),
delayed = $message.find('delay').length > 0,
fullname = this.get('fullname').split(' ')[0],
stamp, time, sender;
if (!body) {
if (composing.length) {
this.messages.add({
fullname: fullname,
sender: 'them',
delayed: delayed,
composing: composing.length
});
}
} else {
if (delayed) {
stamp = $message.find('delay').attr('stamp');
time = (new Date(stamp)).toLocaleTimeString().substring(0,5);
} else {
time = (new Date()).toLocaleTimeString().substring(0,5);
}
if (from == xmppchat.connection.bare_jid) {
fullname = 'me';
sender = 'me';
} else {
sender = 'them';
}
var message = new xmppchat.Message({
fullname: fullname,
sender: sender,
delayed: delayed,
time: time,
message: body
});
this.messages.add(message);
message.save();
}
},
});
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 now = new Date(),
time = now.toLocaleTimeString().substring(0,5),
minutes = now.getMinutes().toString(),
$chat_content = this.$el.find('.chat-content');
/*
* FIXME: we don't use client storage anymore
var msg = xmppchat.storage.getLastMessage(this.model.get('jid'));
if (typeof msg !== 'undefined') {
var prev_date = new Date(Date(msg.split(' ', 2)[0]));
if (this.isDifferentDay(prev_date, now)) {
$chat_content.append($('
'));
$chat_content.append($('').text(now.toString().substring(0,15)));
}
}
*/
message = xmppchat.autoLink(message);
// TODO use minutes logic or remove it
if (minutes.length==1) {minutes = '0'+minutes;}
$chat_content.find('div.chat-event').remove();
$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 (message, replace) {
var $chat_content = this.$el.find('.chat-content');
$chat_content.find('div.chat-event').remove().end()
.append($('').text(message));
$chat_content.scrollTop($chat_content[0].scrollHeight);
},
messageReceived: function (message) {
var $chat_content = this.$el.find('.chat-content');
if (xmppchat.xmppstatus.getStatus() === 'offline') {
// only update the UI if the user is not offline
return;
}
if (message.get('composing')) {
this.insertStatusNotification(message.get('fullname')+' '+'is typing');
return;
} else {
$chat_content.find('div.chat-event').remove();
// TODO use toJSON here
$chat_content.append(
this.message_template({
'sender': message.get('sender'),
'time': message.get('time'),
'message': message.get('message'),
'username': message.get('fullname'),
'extra_classes': message.get('delayed') && 'delayed' || ''
}));
$chat_content.scrollTop($chat_content[0].scrollHeight);
}
xmppchat.updateMsgCounter();
},
isDifferentDay: function (prev_date, next_date) {
return ((next_date.getDate() != prev_date.getDate()) || (next_date.getFullYear() != prev_date.getFullYear()) || (next_date.getMonth() != prev_date.getMonth()));
},
addHelpMessages: function (msgs) {
var $chat_content = this.$el.find('.chat-content'), i,
msgs_length = msgs.length;
for (i=0; i'+msgs[i]+''));
}
this.scrollDown();
},
sendMessage: function (text) {
// 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 timestamp = (new Date()).getTime(),
bare_jid = this.model.get('jid'),
match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/),
msgs;
if (match) {
if (match[1] === "clear") {
this.$el.find('.chat-content').empty();
this.model.messages.reset()
return;
}
else if (match[1] === "help") {
msgs = [
'/help: Show this menu',
'/clear: Remove messages'
];
this.addHelpMessages(msgs);
return;
}
}
var message = $msg({from: xmppchat.connection.bare_jid, to: bare_jid, type: 'chat', id: timestamp})
.c('body').t(text).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
// Forward the message, so that other connected resources are also aware of it.
// TODO: Forward the message only to other connected resources (inside the browser)
var forwarded = $msg({to:xmppchat.connection.bare_jid, type:'chat', id:timestamp})
.c('forwarded', {xmlns:'urn:xmpp:forward:0'})
.c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up()
.cnode(message.tree());
xmppchat.connection.send(message);
xmppchat.connection.send(forwarded);
// Add the new message
var message = new xmppchat.Message({
fullname: 'me',
sender: 'me',
time: (new Date()).toLocaleTimeString().substring(0,5),
message: text
});
this.model.messages.add(message);
message.save();
},
keyPressed: function (ev) {
var $textarea = $(ev.target),
message,
notify,
composing;
if(ev.keyCode == 13) {
ev.preventDefault();
message = $textarea.val();
$textarea.val('').focus();
if (message !== '') {
this.sendMessage(message);
}
this.$el.data('composing', false);
} else {
composing = this.$el.data('composing');
if (!composing) {
if (ev.keyCode != 47) {
// We don't send composing messages if the message
// starts with forward-slash.
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);
}
}
},
closeChat: function () {
this.model.destroy();
},
rosterChanged: function () {
// FIXME: This event handler should go onto the roster itself, then it
// will be called once (for the roster) and not once per open
// chatbox
var fullname = this.model.get('fullname'),
chat_status = item.get('chat_status');
if (item.get('jid') === this.model.get('jid')) {
if (_.has(changed.changes, 'chat_status')) {
if (this.$el.is(':visible')) {
if (chat_status === 'offline') {
this.insertStatusNotification(fullname+' '+'has gone offline');
} else if (chat_status === 'away') {
this.insertStatusNotification(fullname+' '+'has gone away');
} else if ((chat_status === 'dnd')) {
this.insertStatusNotification(fullname+' '+'is busy');
} else if (chat_status === 'online') {
this.$el.find('div.chat-event').remove();
}
}
} else if (_.has(changed.changes, 'status')) {
this.$el.find('p.user-custom-message').text(item.get('status')).attr('title', item.get('status'));
}
}
},
initialize: function (){
// boxviewinit
$('body').append(this.$el.hide());
this.model.messages.on('add', this.messageReceived, this);
this.model.on('destroy', $.proxy(function (model, response, options) {
this.$el.hide('fast');
}, this));
xmppchat.roster.on('change', this.rosterChanged, this);
this.render().show().model.messages.fetch({add: true});
},
template: _.template(
'
'),
initialize: function () {
// TODO see why muc is flagged as unresolved variable
xmppchat.connection.muc.join(
this.model.get('jid'),
this.model.get('nick'),
$.proxy(this.onChatRoomMessage, this),
$.proxy(this.onChatRoomPresence, this),
$.proxy(this.onChatRoomRoster, this));
},
onLeave: function () {
var controlboxview = xmppchat.chatboxesview.views.controlbox;
if (controlboxview) {
controlboxview.roomspanel.trigger('update-rooms-list');
}
},
onChatRoomPresence: function (presence, room) {
// TODO see if nick is useful
var nick = room.nick,
$presence = $(presence),
from = $presence.attr('from');
if ($presence.attr('type') !== 'error') {
// check for status 110 to see if it's our own presence
if ($presence.find("status[code='110']").length) {
// check if server changed our nick
if ($presence.find("status[code='210']").length) {
this.model.set({'nick': Strophe.getResourceFromJid(from)});
}
}
}
return true;
},
onChatRoomMessage: function (message) {
var $message = $(message),
body = $message.children('body').text(),
jid = $message.attr('from'),
composing = $message.find('composing'),
$chat_content = this.$el.find('.chat-content'),
sender = Strophe.unescapeNode(Strophe.getResourceFromJid(jid)),
subject = $message.children('subject').text(),
match;
if (subject) {
this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
}
if (!body) {
if (composing.length) {
this.insertStatusNotification(sender+' '+'is typing');
return true;
}
} else {
if (sender === this.model.get('nick')) {
// Our own message which is already appended
return true;
} else {
$chat_content.find('div.chat-event').remove();
match = body.match(/^\/(.*?)(?: (.*))?$/);
if ((match) && (match[1] === 'me')) {
body = body.replace(/^\/me/, '*'+sender);
$chat_content.append(
this.action_template({
'sender': 'room',
'time': (new Date()).toLocaleTimeString().substring(0,5),
'message': body,
'username': sender,
'extra_classes': ($message.find('delay').length > 0) && 'delayed' || '',
}));
} else {
$chat_content.append(
this.message_template({
'sender': 'room',
'time': (new Date()).toLocaleTimeString().substring(0,5),
'message': body,
'username': sender,
'extra_classes': ($message.find('delay').length > 0) && 'delayed' || ''
}));
}
$chat_content.scrollTop($chat_content[0].scrollHeight);
}
}
return true;
},
onChatRoomRoster: function (roster, room) {
// underscore size is needed because roster is on object
var controlboxview = xmppchat.chatboxesview.views.controlbox,
roster_size = _.size(roster),
$participant_list = this.$el.find('.participant-list'),
participants = [],
i;
if (controlboxview) {
controlboxview.roomspanel.trigger('update-rooms-list');
}
this.$el.find('.participant-list').empty();
for (i=0; i' + Strophe.unescapeNode(_.keys(roster)[i]) + '');
}
$participant_list.append(participants.join(""));
return true;
},
show: function () {
this.$el.css({'opacity': 0,
'display': 'inline'})
.animate({opacity: '1'}, 200);
return this;
},
render: function () {
this.$el.attr('id', this.model.get('box_id'))
.html(this.template(this.model.toJSON()));
return this;
}
});
xmppchat.ChatBoxes = Backbone.Collection.extend({
model: xmppchat.ChatBox,
onConnected: function () {
this.localStorage = new Backbone.LocalStorage(
hex_sha1('converse.chatboxes-'+xmppchat.connection.bare_jid));
if (!this.get('controlbox')) {
this.create({
id: 'controlbox',
box_id: 'controlbox'
});
}
// This will make sure the Roster is set up
this.get('controlbox').set({connected:true})
// Get cached chatboxes from localstorage
this.fetch({
add: true, success:
$.proxy(function (collection, resp) {
if (_.include(_.pluck(resp, 'id'), 'controlbox')) {
// If the controlbox was saved in localstorage, it must be visible
this.get('controlbox').set({visible:true})
}
}, this)
});
},
});
xmppchat.ChatBoxesView = Backbone.View.extend({
el: '#collective-xmpp-chat-data',
createChatRoom: function (jid) {
var box = new xmppchat.ChatRoom(jid, xmppchat.fullname);
var view = new xmppchat.ChatRoomView({
'model': box
});
this.views[jid] = view.render();
view.$el.appendTo(this.$el);
this.options.model.add(box);
return view;
},
createChatBox: function (data) {
var model = new xmppchat.ChatBox({
'id': data['jid'],
'jid': data['jid'],
'fullname': data['fullname'],
'image_type': data['image_type'],
'image': data['image'],
'visible': data['visible'],
'url': data['url'],
});
var chatboxes = this.options.model.add(model);
model.save();
return model;
},
openControlBox: function () {
if (this.model.get('controlbox')) {
this.showChat('controlbox');
} else {
this.options.model.add({
id: 'controlbox',
box_id: 'controlbox',
visible: true
});
}
},
openChat: function (roster_item) {
var jid = roster_item.get('jid');
jid = Strophe.getBareJidFromJid(jid);
if (this.model.get(jid)) {
this.showChat(jid);
} else {
this.createChatBox({
'jid': jid,
'fullname': roster_item.get('fullname'),
'image': roster_item.get('image'),
'image_type': roster_item.get('image_type'),
'visible': true,
'url': roster_item.get('url'),
})
}
},
showChat: function (jid) {
var view = this.views[jid];
view.model.set({visible:true})
if (view.isVisible()) {
view.focus();
} else {
view.show();
if (jid !== 'controlbox') {
view.scrollDown();
view.focus();
}
}
return view;
},
messageReceived: function (message) {
var partner_jid, $message = $(message),
message_from = $message.attr('from');
if ( message_from == xmppchat.connection.jid) {
// FIXME: Forwarded messages should be sent to specific resources, not broadcasted
return true;
}
var $forwarded = $message.children('forwarded');
if ($forwarded.length) {
$message = $forwarded.children('message');
}
var from = Strophe.getBareJidFromJid(message_from),
to = Strophe.getBareJidFromJid($message.attr('to')),
view, resource, chatbox;
if (from == xmppchat.connection.bare_jid) {
// I am the sender, so this must be a forwarded message...
partner_jid = to;
resource = Strophe.getResourceFromJid($message.attr('to'));
} else {
partner_jid = from;
resource = Strophe.getResourceFromJid(message_from);
}
view = this.views[partner_jid];
if (!view) {
xmppchat.getVCard(
partner_jid,
$.proxy(function (jid, fullname, img, img_type, url) {
chatbox = this.createChatBox({
'jid': jid,
'fullname': fullname,
'image': img,
'image_type': img_type,
'url': url,
})
chatbox.messageReceived(message);
xmppchat.roster.addResource(partner_jid, resource);
}, this),
$.proxy(function () {
// # TODO: call the function above
console.log("An error occured while fetching vcard");
}, this));
return true;
} else if (!view.isVisible()) {
this.showChat(partner_jid);
}
view.model.messageReceived(message);
xmppchat.roster.addResource(partner_jid, resource);
return true;
},
initialize: function () {
// boxesviewinit
this.views = {};
this.options.model.on("add", function (item) {
var view;
if (item.get('box_id') === 'controlbox') {
view = new xmppchat.ControlBoxView({model: item});
} else {
view = new xmppchat.ChatBoxView({model: item});
}
this.views[item.get('id')] = view;
view.$el.appendTo(this.$el);
}, this);
}
});
xmppchat.RosterItem = Backbone.Model.extend({
initialize: function (attributes, options) {
var jid = attributes['jid'];
if (!attributes['fullname']) {
attributes['fullname'] = jid;
}
_.extend(attributes, {
'id': jid,
'user_id': Strophe.getNodeFromJid(jid),
'resources': [],
'chat_status': 'offline',
'status': 'offline',
'sorted': false,
});
this.set(attributes);
},
});
xmppchat.RosterItemView = Backbone.View.extend({
tagName: 'dd',
events: {
"click .accept-xmpp-request": "acceptRequest",
"click .decline-xmpp-request": "declineRequest",
"click .open-chat": "openChat",
"click .remove-xmpp-contact": "removeContact"
},
openChat: function (ev) {
xmppchat.chatboxesview.openChat(this.model);
ev.preventDefault();
},
removeContact: function (ev) {
ev.preventDefault();
var result = confirm("Are you sure you want to remove this contact?");
if (result==true) {
var bare_jid = this.model.get('jid');
xmppchat.connection.roster.remove(bare_jid, function (iq) {
xmppchat.connection.roster.unauthorize(bare_jid);
xmppchat.rosterview.model.remove(bare_jid);
});
}
},
acceptRequest: function (ev) {
var jid = this.model.get('jid');
xmppchat.connection.roster.authorize(jid);
xmppchat.connection.roster.add(jid, this.model.get('fullname'), [], function (iq) {
xmppchat.connection.roster.subscribe(jid);
});
ev.preventDefault();
},
declineRequest: function (ev) {
var that = this;
xmppchat.connection.roster.unauthorize(this.model.get('jid'));
that.trigger('decline-request', that.model);
ev.preventDefault();
},
template: _.template(
'{{ fullname }}' +
''),
pending_template: _.template(
'{{ fullname }}' +
''),
request_template: _.template('{{ fullname }}' +
'' +
'' +
''),
render: function () {
var item = this.model,
ask = item.get('ask'),
subscription = item.get('subscription');
this.$el.addClass(item.get('chat_status'));
if (ask === 'subscribe') {
this.$el.addClass('pending-xmpp-contact');
this.$el.html(this.pending_template(item.toJSON()));
} else if (ask === 'request') {
this.$el.addClass('requesting-xmpp-contact');
this.$el.html(this.request_template(item.toJSON()));
xmppchat.chatboxesview.showChat('controlbox');
} else if (subscription === 'both' || subscription === 'to') {
this.$el.addClass('current-xmpp-contact');
this.$el.html(this.template(item.toJSON()));
}
return this;
},
initialize: function () {
this.options.model.on('change', function (item, changed) {
if (_.has(changed.changes, 'chat_status')) {
this.$el.attr('class', item.changed.chat_status);
}
}, this);
}
});
xmppchat.getVCard = function (jid, callback, errback) {
/* First we check if we don't already have a RosterItem, since it will
* contain all the vCard details.
*/
var model = xmppchat.roster.getItem(jid);
if (model) {
callback(
model.get('jid'),
model.get('fullname'),
model.get('image'),
model.get('image_type'),
model.get('url')
)
} else {
xmppchat.connection.vcard.get($.proxy(function (iq) {
$vcard = $(iq).find('vCard');
var fullname = $vcard.find('FN').text(),
img = $vcard.find('BINVAL').text(),
img_type = $vcard.find('TYPE').text(),
url = $vcard.find('URL').text();
callback(jid, fullname, img, img_type, url);
}, this), jid, errback())
}
}
xmppchat.RosterItems = Backbone.Collection.extend({
model: xmppchat.RosterItem,
comparator : function (rosteritem) {
var chat_status = rosteritem.get('chat_status'),
rank = 4;
switch(chat_status) {
case 'offline':
rank = 0;
break;
case 'unavailable':
rank = 1;
break;
case 'xa':
rank = 2;
break;
case 'away':
rank = 3;
break;
case 'dnd':
rank = 4;
break;
case 'online':
rank = 5;
break;
}
return rank;
},
subscribeToSuggestedItems: function (msg) {
$(msg).find('item').each(function () {
var $this = $(this),
jid = $this.attr('jid'),
action = $this.attr('action'),
fullname = $this.attr('name');
if (action === 'add') {
xmppchat.connection.roster.add(jid, fullname, [], function (iq) {
xmppchat.connection.roster.subscribe(jid);
});
}
});
return true;
},
isSelf: function (jid) {
return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(xmppchat.connection.jid));
},
getItem: function (id) {
return Backbone.Collection.prototype.get.call(this, id);
},
addRosterItem: function (attributes) {
var model = new xmppchat.RosterItem(attributes)
this.add(model);
model.save();
},
addResource: function (bare_jid, resource) {
var item = this.getItem(bare_jid),
resources;
if (item) {
resources = item.get('resources');
if (resources) {
if (_.indexOf(resources, resource) == -1) {
resources.push(resource);
item.set({'resources': resources});
}
} else {
item.set({'resources': [resource]});
}
}
},
removeResource: function (bare_jid, resource) {
var item = this.getItem(bare_jid),
resources,
idx;
if (item) {
resources = item.get('resources');
idx = _.indexOf(resources, resource);
if (idx !== -1) {
resources.splice(idx, 1);
item.set({'resources': resources});
return resources.length;
}
}
return 0;
},
clearResources: function (bare_jid) {
var item = this.getItem(bare_jid);
if (item) {
item.set({'resources': []});
}
},
getTotalResources: function (bare_jid) {
var item = this.getItem(bare_jid);
if (item) {
return _.size(item.get('resources'));
}
return 0;
},
subscribeBack: function (jid) {
// XXX: Why the distinction between jid and bare_jid?
var bare_jid = Strophe.getBareJidFromJid(jid)
if (xmppchat.connection.roster.findItem(bare_jid)) {
xmppchat.connection.roster.authorize(bare_jid);
xmppchat.connection.roster.subscribe(jid);
} else {
xmppchat.connection.roster.add(jid, '', [], function (iq) {
xmppchat.connection.roster.authorize(bare_jid);
xmppchat.connection.roster.subscribe(jid);
});
}
},
unsubscribe: function (jid) {
/* Upon receiving the presence stanza of type "unsubscribed",
* the user SHOULD acknowledge receipt of that subscription state
* notification by sending a presence stanza of type "unsubscribe"
* this step lets the user's server know that it MUST no longer
* send notification of the subscription state change to the user.
*/
xmppchat.xmppstatus.sendPresence('unsubscribe');
if (xmppchat.connection.roster.findItem(jid)) {
xmppchat.connection.roster.remove(bare_jid, function (iq) {
xmppchat.rosterview.model.remove(bare_jid);
});
}
},
getNumOnlineContacts: function () {
var count = 0,
models = this.models,
models_length = models.length;
for (var i=0; iContact requests' +
'
My contacts
' +
'
Pending contacts
'),
render: function (item) {
var $my_contacts = this.$el.find('#xmpp-contacts'),
$contact_requests = this.$el.find('#xmpp-contact-requests'),
$pending_contacts = this.$el.find('#pending-xmpp-contacts'),
$count, presence_change;
if (item) {
var jid = item.id,
view = this.rosteritemviews[item.id],
ask = item.get('ask'),
subscription = item.get('subscription'),
crit = {order:'asc'};
if (ask === 'subscribe') {
$pending_contacts.after(view.render().el);
$pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
} else if (ask === 'request') {
$contact_requests.after(view.render().el);
$contact_requests.after($contact_requests.siblings('dd.requesting-xmpp-contact').tsort(crit));
} else if (subscription === 'both' || subscription === 'to') {
if (!item.get('sorted')) {
// this attribute will be true only after all of the elements have been added on the page
// at this point all offline
$my_contacts.after(view.render().el);
}
else {
// just by calling render will be enough to change the icon of the existing item without
// having to reinsert it and the sort will come from the presence change
view.render();
}
}
presence_change = view.model.changed['chat_status'];
if (presence_change) {
// resort all items only if the model has changed it's chat_status as this render
// is also triggered when the resource is changed which always comes before the presence change
// therefore we avoid resorting when the change doesn't affect the position of the item
$my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
$my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
$my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.away').tsort('a', crit));
$my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.dnd').tsort('a', crit));
$my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.online').tsort('a', crit));
}
if (item.get('is_last') && !item.get('sorted')) {
// this will be true after all of the roster items have been added with the default
// options where all of the items are offline and now we can show the rosterView
item.set('sorted', true)
this.initialSort();
this.$el.show();
}
}
// Hide the headings if there are no contacts under them
_.each([$my_contacts, $contact_requests, $pending_contacts], function (h) {
if (h.nextUntil('dt').length) {
h.show();
}
else {
h.hide();
}
});
$count = $('#online-count');
$count.text(this.model.getNumOnlineContacts());
return this;
},
initialSort: function () {
var $my_contacts = this.$el.find('#xmpp-contacts'),
crit = {order:'asc'};
$my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
$my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
},
});
xmppchat.XMPPStatus = Backbone.Model.extend({
initialize: function () {
this.set({
'status' : this.getStatus(),
'status_message' : this.getStatusMessage()
});
},
initStatus: function () {
/* Called when the page is loaded and we aren't sure what the users
* status is. Will also cause the UI to be updated with the correct
* status.
*/
var stat = this.getStatus();
if (stat === undefined) {
this.setStatus('online');
} else {
this.sendPresence(stat);
}
},
sendPresence: function (type) {
var status_message = this.getStatusMessage(),
presence;
if (type === 'unavailable') {
presence = $pres({'type':type});
} else {
if (type === 'online') {
presence = $pres();
} else {
presence = $pres().c('show').t(type);
}
if (status_message) {
presence.c('status').t(status_message)
}
}
xmppchat.connection.send(presence);
},
getStatus: function () {
return store.get(xmppchat.connection.bare_jid+'-xmpp-status');
},
setStatus: function (value) {
this.sendPresence(value);
this.set({'status': value});
store.set(xmppchat.connection.bare_jid+'-xmpp-status', value);
},
getStatusMessage: function () {
return store.get(xmppchat.connection.bare_jid+'-xmpp-custom-status');
},
setStatusMessage: function (status_message) {
xmppchat.connection.send($pres().c('show').t(this.getStatus()).up().c('status').t(status_message));
this.set({'status_message': status_message});
store.set(xmppchat.connection.bare_jid+'-xmpp-custom-status', status_message);
}
});
xmppchat.XMPPStatusView = Backbone.View.extend({
el: "span#xmpp-status-holder",
events: {
"click a.choose-xmpp-status": "toggleOptions",
"click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
"submit #set-custom-xmpp-status": "setStatusMessage",
"click .dropdown dd ul li a": "setStatus"
},
toggleOptions: function (ev) {
ev.preventDefault();
$(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
},
change_status_message_template: _.template(
''),
status_template: _.template(
'
'),
initialize: function () {
this.model.initStatus();
// Listen for status change on the model and initialize
this.options.model.on("change", $.proxy(this.updateStatusUI, this));
},
render: function () {
// Replace the default dropdown with something nicer
var $select = this.$el.find('select#select-xmpp-status'),
chat_status = this.model.getStatus() || 'offline',
options = $('option', $select),
$options_target,
options_list = [],
that = this;
this.$el.html(this.choose_template());
this.$el.find('#fancy-xmpp-status-select')
.html(this.status_template({
'status_message': "I am " + this.getPrettyStatus(chat_status),
'chat_status': chat_status
}));
// iterate through all the