diff --git a/Gruntfile.js b/Gruntfile.js
index e3470db16..956f3ba31 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -99,10 +99,15 @@ module.exports = function(grunt) {
done();
};
exec('./node_modules/requirejs/bin/r.js -o src/build.js && ' +
+ './node_modules/requirejs/bin/r.js -o src/build.js optimize=none out=builds/converse.js && ' +
'./node_modules/requirejs/bin/r.js -o src/build-no-locales-no-otr.js && ' +
+ './node_modules/requirejs/bin/r.js -o src/build-no-locales-no-otr.js optimize=none out=builds/converse-no-locales-no-otr.js && ' +
'./node_modules/requirejs/bin/r.js -o src/build-no-otr.js &&' +
+ './node_modules/requirejs/bin/r.js -o src/build-no-otr.js optimize=none out=builds/converse-no-otr.js && ' +
'./node_modules/requirejs/bin/r.js -o src/build-website-no-otr.js &&' +
'./node_modules/requirejs/bin/r.js -o src/build-website.js', callback);
+ // XXX: It might be possible to not have separate build config files. For example:
+ // 'r.js -o src/build.js paths.converse-dependencies=src/deps-no-otr paths.locales=locale/nolocales out=builds/converse-no-locales-no-otr.min.js'
});
grunt.registerTask('minify', 'Create a new release', ['cssmin', 'jsmin']);
diff --git a/bower.json b/bower.json
index 594e1c7bf..5eb1b414c 100644
--- a/bower.json
+++ b/bower.json
@@ -16,7 +16,6 @@
"backbone.browserStorage": "*",
"backbone.overview": "*",
"strophe": "~1.1.3",
- "strophe.roster": "https://raw.github.com/strophe/strophejs-plugins/b1f364eb6e854ffe844c57add38e885cfeb9b498/roster/strophe.roster.js",
"strophe.muc": "https://raw.githubusercontent.com/strophe/strophejs-plugins/master/muc/strophe.muc.js",
"otr": "0.2.12",
"crypto-js-evanvosberg": "~3.1.2",
@@ -30,7 +29,7 @@
"bootstrapJS": "https://raw.githubusercontent.com/jcbrand/bootstrap/7d96a5f60d26c67b5348b270a775518b96a702c8/dist/js/bootstrap.js",
"fontawesome": "~4.1.0",
"typeahead.js": "https://raw.githubusercontent.com/jcbrand/typeahead.js/eedfb10505dd3a20123d1fafc07c1352d83f0ab3/dist/typeahead.jquery.js",
- "strophejs-plugins": "~0.0.4"
+ "strophejs-plugins": "https://github.com/strophe/strophejs-plugins.git#a56421ff4ecf0807113ab48c46728715597df599"
},
"exportsOverride": {}
}
diff --git a/converse.js b/converse.js
index 41b41be00..2a79366a6 100644
--- a/converse.js
+++ b/converse.js
@@ -541,6 +541,7 @@
};
this.clearSession = function () {
+ this.roster.browserStorage._clear();
this.session.browserStorage._clear();
// XXX: this should perhaps go into the beforeunload handler
converse.chatboxes.get('controlbox').save({'connected': false});
@@ -1935,8 +1936,16 @@
b64_sha1('converse.roster.groups'+converse.bare_jid));
converse.rosterview = new converse.RosterView({model: rostergroups});
this.contactspanel.$el.append(converse.rosterview.$el);
+ // TODO:
+ // See if we shouldn't also fetch the roster here... otherwise
+ // the roster is always populated by the rosterHandler method,
+ // which appears to be a less economic way.
+ // i.e. from what it seems, only groups are fetched from
+ // browserStorage, and no contacts.
+ // XXX: Make sure that if fetch is called, we don't sort on
+ // each item add...
+ // converse.roster.fetch()
converse.rosterview.render().fetch().update();
- converse.connection.roster.get(function () {});
return this;
},
@@ -3212,6 +3221,10 @@
'status': ''
}, attributes);
this.set(attrs);
+ },
+
+ showInRoster: function () {
+ return (!converse.show_only_online_users || this.get('chat_status') === 'online');
}
});
@@ -3226,24 +3239,12 @@
},
initialize: function () {
- this.model.on("change", this.onChange, this);
+ this.model.on("change", this.render, this);
this.model.on("remove", this.remove, this);
this.model.on("destroy", this.remove, this);
this.model.on("open", this.openChat, this);
},
- onChange: function () {
- if (converse.show_only_online_users) {
- if (this.model.get('chat_status') !== 'online') {
- this.$el.hide();
- } else {
- this.$el.show();
- }
- } else {
- this.render();
- }
- },
-
openChat: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
// XXX: Can this.model.attributes be used here, instead of
@@ -3293,6 +3294,12 @@
},
render: function () {
+ if (!this.model.showInRoster()) {
+ this.$el.hide();
+ return this;
+ } else if (this.$el[0].style.display === "none") {
+ this.$el.show();
+ }
var item = this.model,
ask = item.get('ask'),
chat_status = item.get('chat_status'),
@@ -3356,13 +3363,13 @@
this.RosterContacts = Backbone.Collection.extend({
model: converse.RosterContact,
-
comparator: function (contact1, contact2) {
- var name1 = contact1.get('fullname').toLowerCase();
+ var name1, name2;
var status1 = contact1.get('chat_status') || 'offline';
- var name2 = contact2.get('fullname').toLowerCase();
var status2 = contact2.get('chat_status') || 'offline';
if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
+ name1 = contact1.get('fullname').toLowerCase();
+ name2 = contact2.get('fullname').toLowerCase();
return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
} else {
return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
@@ -3370,15 +3377,13 @@
},
subscribeToSuggestedItems: function (msg) {
- $(msg).find('item').each(function () {
+ $(msg).find('item').each(function (i, items) {
var $this = $(this),
jid = $this.attr('jid'),
action = $this.attr('action'),
fullname = $this.attr('name');
if (action === 'add') {
- converse.connection.roster.add(jid, fullname, [], function (iq) {
- converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
- });
+ converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
}
});
return true;
@@ -3486,7 +3491,8 @@
rosterHandler: function (items, item) {
converse.emit('roster', items);
this.clearCache(items);
- _.each(items, function (item, index, items) {
+ var new_items = item ? [item] : items;
+ _.each(new_items, function (item, index, items) {
if (this.isSelf(item.jid)) { return; }
var model = this.get(item.jid);
if (!model) {
@@ -3503,7 +3509,7 @@
groups: item.groups,
jid: item.jid,
subscription: item.subscription
- });
+ }, {sort: false});
} else {
if ((item.subscription === 'none') && (item.ask === null)) {
// This user is no longer in our roster
@@ -3686,11 +3692,13 @@
var view = new converse.RosterContactView({model: contact});
this.add(contact.get('id'), view);
view = this.positionContact(contact).render();
- if (this.model.get('state') === CLOSED) {
- view.$el.hide();
- this.$el.show();
- } else {
- this.show();
+ if (contact.showInRoster()) {
+ if (this.model.get('state') === CLOSED) {
+ if (view.$el[0].style.display !== "none") { view.$el.hide(); }
+ if (this.$el[0].style.display === "none") { this.$el.show(); }
+ } else {
+ if (this.$el[0].style.display !== "block") { this.show(); }
+ }
}
},
@@ -3712,6 +3720,9 @@
},
show: function () {
+ // FIXME: There's a bug here, if show_only_online_users is true
+ // Possible solution, get the group, call _.each and check
+ // showInRoster
this.$el.nextUntil('dt').addBack().show();
},
@@ -3729,7 +3740,7 @@
if (q.length === 0) {
if (this.model.get('state') === OPENED) {
this.model.contacts.each($.proxy(function (item) {
- if (!(converse.show_only_online_users && item.get('chat_status') === 'online')) {
+ if (item.showInRoster()) {
this.get(item.get('id')).$el.show();
}
}, this));
@@ -3849,16 +3860,20 @@
converse.roster.on("remove", this.update, this);
this.model.on("add", this.onGroupAdd, this);
this.model.on("reset", this.reset, this);
+ this.$roster = $('
');
},
- update: function () {
+ update: _.debounce(function () {
var $count = $('#online-count');
$count.text('('+converse.roster.getNumOnlineContacts()+')');
if (!$count.is(':visible')) {
$count.show();
}
+ if (this.$roster.parent().length === 0) {
+ this.$el.append(this.$roster.show());
+ }
return this.showHideFilter();
- },
+ }, converse.animate ? 100 : 0),
render: function () {
this.$el.html(converse.templates.roster({
@@ -3871,10 +3886,47 @@
fetch: function () {
this.model.fetch({
- silent: true,
- success: $.proxy(this.positionFetchedGroups, this)
+ silent: true, // We use the success handler to handle groups that were added,
+ // we need to first have all groups before positionFetchedGroups
+ // will work properly.
+ success: $.proxy(function (collection, resp, options) {
+ if (collection.length !== 0) {
+ this.positionFetchedGroups(collection, resp, options);
+ }
+ converse.roster.fetch({
+ add: true,
+ success: function (collection) {
+ // XXX: Bit of a hack.
+ // strophe.roster expects .get to be called for
+ // every page load so that its "items" attr
+ // gets populated.
+ // This is very inefficient for large rosters,
+ // and we already have the roster cached in
+ // sessionStorage.
+ // Therefore we manually populate the "items"
+ // attr.
+ // Ideally we should eventually replace
+ // strophe.roster with something better.
+ if (collection.length > 0) {
+ collection.each(function (item) {
+ converse.connection.roster.items.push({
+ name : item.get('fullname'),
+ jid : item.get('jid'),
+ subscription : item.get('subscription'),
+ ask : item.get('ask'),
+ groups : item.get('groups'),
+ resources : item.get('resources')
+ });
+ });
+ converse.initial_presence_sent = 1;
+ converse.xmppstatus.sendPresence();
+ } else {
+ converse.connection.roster.get();
+ }
+ }
+ });
+ }, this)
});
- converse.roster.fetch({add: true});
return this;
},
@@ -3942,7 +3994,7 @@
// Don't hide if user is currently filtering.
return;
}
- if (this.$('.roster-contacts').hasScrollBar()) {
+ if (this.$roster.hasScrollBar()) {
if (!visible) {
$filter.show();
$type.show();
@@ -3957,6 +4009,7 @@
reset: function () {
converse.roster.reset();
this.removeAll();
+ this.$roster = $('
');
this.render().update();
return this;
},
@@ -3964,13 +4017,24 @@
registerRosterHandler: function () {
// Register handlers that depend on the roster
converse.connection.roster.registerCallback(
- $.proxy(converse.roster.rosterHandler, converse.roster),
- null, 'presence', null);
+ $.proxy(converse.roster.rosterHandler, converse.roster)
+ );
},
registerRosterXHandler: function () {
+ var t = 0;
converse.connection.addHandler(
- $.proxy(converse.roster.subscribeToSuggestedItems, converse.roster),
+ function (msg) {
+ window.setTimeout(
+ function () {
+ converse.connection.flush();
+ $.proxy(converse.roster.subscribeToSuggestedItems, converse.roster)(msg);
+ },
+ t
+ );
+ t += $(msg).find('item').length*250;
+ return true;
+ },
'http://jabber.org/protocol/rosterx', 'message', null);
},
@@ -4047,7 +4111,7 @@
this.add(group.get('name'), view.render());
}
if (idx === 0) {
- this.$('.roster-contacts').append(view.$el);
+ this.$roster.append(view.$el);
} else {
this.appendGroup(view);
}
@@ -4058,13 +4122,14 @@
/* Place the group's DOM element in the correct alphabetical
* position amongst the other groups in the roster.
*/
- var index = this.model.indexOf(view.model);
+ var $groups = this.$roster.find('.roster-group'),
+ index = $groups.length ? this.model.indexOf(view.model) : 0;
if (index === 0) {
- this.$('.roster-contacts').prepend(view.$el);
+ this.$roster.prepend(view.$el);
} else if (index == (this.model.length-1)) {
this.appendGroup(view);
} else {
- $(this.$('.roster-group').eq(index)).before(view.$el);
+ $($groups.eq(index)).before(view.$el);
}
return this;
},
@@ -4072,7 +4137,7 @@
appendGroup: function (view) {
/* Add the group at the bottom of the roster
*/
- var $last = this.$('.roster-group').last();
+ var $last = this.$roster.find('.roster-group').last();
var $siblings = $last.siblings('dd');
if ($siblings.length > 0) {
$siblings.last().after(view.$el);
@@ -4563,8 +4628,6 @@
sid = this.session.get('sid');
jid = this.session.get('jid');
if (rid && jid && sid) {
- // We have the necessary tokens for resuming a session
- rid += 1;
this.session.save({rid: rid}); // The RID needs to be increased with each request.
this.connection.attach(jid, sid, rid, this.onConnect);
} else if (this.prebind) {
diff --git a/index.html b/index.html
index 520483ac5..81ab42de0 100644
--- a/index.html
+++ b/index.html
@@ -229,7 +229,6 @@
diff --git a/main.js b/main.js
index adca6f286..48f0663b8 100644
--- a/main.js
+++ b/main.js
@@ -1,4 +1,4 @@
-config = {
+require.config({
baseUrl: '.',
paths: {
"backbone": "components/backbone/backbone",
@@ -17,7 +17,7 @@ config = {
"strophe": "components/strophe/strophe",
"strophe.disco": "components/strophejs-plugins/disco/strophe.disco",
"strophe.muc": "components/strophe.muc/index",
- "strophe.roster": "components/strophe.roster/index",
+ "strophe.roster": "src/strophe.roster",
"strophe.vcard": "components/strophejs-plugins/vcard/strophe.vcard",
"text": 'components/requirejs-text/text',
"tpl": 'components/requirejs-tpl-jcbrand/tpl',
@@ -139,16 +139,13 @@ config = {
'crypto.sha1': { deps: ['crypto.core'] },
'crypto.sha256': { deps: ['crypto.core'] },
'bigint': { deps: ['crypto'] },
+ 'strophe': { exports: 'Strophe' },
'strophe.disco': { deps: ['strophe'] },
'strophe.muc': { deps: ['strophe'] },
'strophe.roster': { deps: ['strophe'] },
'strophe.vcard': { deps: ['strophe'] }
}
-};
-
-if (typeof(require) !== 'undefined') {
- require.config(config);
- require(["jquery", "converse"], function($, converse) {
- window.converse = converse;
- });
-}
+});
+require(["converse"], function(converse) {
+ window.converse = converse;
+});
diff --git a/spec/chatbox.js b/spec/chatbox.js
index ac4dc0518..4e0eb6dbc 100644
--- a/spec/chatbox.js
+++ b/spec/chatbox.js
@@ -57,23 +57,26 @@
expect($("#conversejs .chatbox").length).toBe(1); // Controlbox is open
// Test that they can be trimmed
- var online_contacts = this.rosterview.$el.find('dt.roster-group').siblings('dd.current-xmpp-contact').find('a.open-chat');
- for (i=0; i
+*/
+/**
+ * Roster Plugin
+ * Allow easily roster management
+ *
+ * Features
+ * * Get roster from server
+ * * handle presence
+ * * handle roster iq
+ * * subscribe/unsubscribe
+ * * authorize/unauthorize
+ * * roster versioning (xep 237)
+ */
+Strophe.addConnectionPlugin('roster',
+{
+ /** Function: init
+ * Plugin init
+ *
+ * Parameters:
+ * (Strophe.Connection) conn - Strophe connection
+ */
+ init: function(conn)
+ {
+ this._connection = conn;
+ this._callbacks = [];
+ /** Property: items
+ * Roster items
+ * [
+ * {
+ * name : "",
+ * jid : "",
+ * subscription : "",
+ * ask : "",
+ * groups : ["", ""],
+ * resources : {
+ * myresource : {
+ * show : "",
+ * status : "",
+ * priority : ""
+ * }
+ * }
+ * }
+ * ]
+ */
+ this.items = [];
+ /** Property: ver
+ * current roster revision
+ * always null if server doesn't support xep 237
+ */
+ this.ver = null;
+ // Override the connect and attach methods to always add presence and roster handlers.
+ // They are removed when the connection disconnects, so must be added on connection.
+ var oldCallback, roster = this, _connect = conn.connect, _attach = conn.attach;
+ var newCallback = function(status)
+ {
+ if (status == Strophe.Status.ATTACHED || status == Strophe.Status.CONNECTED)
+ {
+ try
+ {
+ // Presence subscription
+ conn.addHandler(roster._onReceivePresence.bind(roster), null, 'presence', null, null, null);
+ conn.addHandler(roster._onReceiveIQ.bind(roster), Strophe.NS.ROSTER, 'iq', "set", null, null);
+ }
+ catch (e)
+ {
+ Strophe.error(e);
+ }
+ }
+ if (typeof oldCallback === "function") {
+ oldCallback.apply(this, arguments);
+ }
+ };
+ conn.connect = function(jid, pass, callback, wait, hold)
+ {
+ oldCallback = callback;
+ if (typeof jid == "undefined")
+ jid = null;
+ if (typeof pass == "undefined")
+ pass = null;
+ callback = newCallback;
+ _connect.apply(conn, [jid, pass, callback, wait, hold]);
+ };
+ conn.attach = function(jid, sid, rid, callback, wait, hold, wind)
+ {
+ oldCallback = callback;
+ if (typeof jid == "undefined")
+ jid = null;
+ if (typeof sid == "undefined")
+ sid = null;
+ if (typeof rid == "undefined")
+ rid = null;
+ callback = newCallback;
+ _attach.apply(conn, [jid, sid, rid, callback, wait, hold, wind]);
+ };
+
+ Strophe.addNamespace('ROSTER_VER', 'urn:xmpp:features:rosterver');
+ Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
+ },
+ /** Function: supportVersioning
+ * return true if roster versioning is enabled on server
+ */
+ supportVersioning: function()
+ {
+ return (this._connection.features && this._connection.features.getElementsByTagName('ver').length > 0);
+ },
+ /** Function: get
+ * Get Roster on server
+ *
+ * Parameters:
+ * (Function) userCallback - callback on roster result
+ * (String) ver - current rev of roster
+ * (only used if roster versioning is enabled)
+ * (Array) items - initial items of ver
+ * (only used if roster versioning is enabled)
+ * In browser context you can use sessionStorage
+ * to store your roster in json (JSON.stringify())
+ */
+ get: function(userCallback, ver, items)
+ {
+ var attrs = {xmlns: Strophe.NS.ROSTER};
+ if (this.supportVersioning())
+ {
+ // empty rev because i want an rev attribute in the result
+ attrs.ver = ver || '';
+ this.items = items || [];
+ }
+ var iq = $iq({type: 'get', 'id' : this._connection.getUniqueId('roster')}).c('query', attrs);
+ return this._connection.sendIQ(iq,
+ this._onReceiveRosterSuccess.bind(this, userCallback),
+ this._onReceiveRosterError.bind(this, userCallback));
+ },
+ /** Function: registerCallback
+ * register callback on roster (presence and iq)
+ *
+ * Parameters:
+ * (Function) call_back
+ */
+ registerCallback: function(call_back)
+ {
+ this._callbacks.push(call_back);
+ },
+ /** Function: findItem
+ * Find item by JID
+ *
+ * Parameters:
+ * (String) jid
+ */
+ findItem : function(jid)
+ {
+ try {
+ for (var i = 0; i < this.items.length; i++)
+ {
+ if (this.items[i] && this.items[i].jid == jid)
+ {
+ return this.items[i];
+ }
+ }
+ } catch (e)
+ {
+ Strophe.error(e);
+ }
+ return false;
+ },
+ /** Function: removeItem
+ * Remove item by JID
+ *
+ * Parameters:
+ * (String) jid
+ */
+ removeItem : function(jid)
+ {
+ for (var i = 0; i < this.items.length; i++)
+ {
+ if (this.items[i] && this.items[i].jid == jid)
+ {
+ this.items.splice(i, 1);
+ return true;
+ }
+ }
+ return false;
+ },
+ /** Function: subscribe
+ * Subscribe presence
+ *
+ * Parameters:
+ * (String) jid
+ * (String) message (optional)
+ * (String) nick (optional)
+ */
+ subscribe: function(jid, message, nick) {
+ var pres = $pres({to: jid, type: "subscribe"});
+ if (message && message !== "") {
+ pres.c("status").t(message).up();
+ }
+ if (nick && nick !== "") {
+ pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
+ }
+ this._connection.send(pres);
+ },
+ /** Function: unsubscribe
+ * Unsubscribe presence
+ *
+ * Parameters:
+ * (String) jid
+ * (String) message
+ */
+ unsubscribe: function(jid, message)
+ {
+ var pres = $pres({to: jid, type: "unsubscribe"});
+ if (message && message !== "")
+ pres.c("status").t(message);
+ this._connection.send(pres);
+ },
+ /** Function: authorize
+ * Authorize presence subscription
+ *
+ * Parameters:
+ * (String) jid
+ * (String) message
+ */
+ authorize: function(jid, message)
+ {
+ var pres = $pres({to: jid, type: "subscribed"});
+ if (message && message !== "")
+ pres.c("status").t(message);
+ this._connection.send(pres);
+ },
+ /** Function: unauthorize
+ * Unauthorize presence subscription
+ *
+ * Parameters:
+ * (String) jid
+ * (String) message
+ */
+ unauthorize: function(jid, message)
+ {
+ var pres = $pres({to: jid, type: "unsubscribed"});
+ if (message && message !== "")
+ pres.c("status").t(message);
+ this._connection.send(pres);
+ },
+ /** Function: add
+ * Add roster item
+ *
+ * Parameters:
+ * (String) jid - item jid
+ * (String) name - name
+ * (Array) groups
+ * (Function) call_back
+ */
+ add: function(jid, name, groups, call_back)
+ {
+ var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: jid,
+ name: name});
+ for (var i = 0; i < groups.length; i++)
+ {
+ iq.c('group').t(groups[i]).up();
+ }
+ this._connection.sendIQ(iq, call_back, call_back);
+ },
+ /** Function: update
+ * Update roster item
+ *
+ * Parameters:
+ * (String) jid - item jid
+ * (String) name - name
+ * (Array) groups
+ * (Function) call_back
+ */
+ update: function(jid, name, groups, call_back)
+ {
+ var item = this.findItem(jid);
+ if (!item)
+ {
+ throw "item not found";
+ }
+ var newName = name || item.name;
+ var newGroups = groups || item.groups;
+ var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
+ name: newName});
+ for (var i = 0; i < newGroups.length; i++)
+ {
+ iq.c('group').t(newGroups[i]).up();
+ }
+ return this._connection.sendIQ(iq, call_back, call_back);
+ },
+ /** Function: remove
+ * Remove roster item
+ *
+ * Parameters:
+ * (String) jid - item jid
+ * (Function) call_back
+ */
+ remove: function(jid, call_back)
+ {
+ var item = this.findItem(jid);
+ if (!item)
+ {
+ throw "item not found";
+ }
+ var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
+ subscription: "remove"});
+ this._connection.sendIQ(iq, call_back, call_back);
+ },
+ /** PrivateFunction: _onReceiveRosterSuccess
+ *
+ */
+ _onReceiveRosterSuccess: function(userCallback, stanza)
+ {
+ this._updateItems(stanza);
+ if (typeof userCallback === "function") {
+ userCallback(this.items);
+ }
+ },
+ /** PrivateFunction: _onReceiveRosterError
+ *
+ */
+ _onReceiveRosterError: function(userCallback, stanza)
+ {
+ userCallback(this.items);
+ },
+ /** PrivateFunction: _onReceivePresence
+ * Handle presence
+ */
+ _onReceivePresence : function(presence)
+ {
+ // TODO: from is optional
+ var jid = presence.getAttribute('from');
+ var from = Strophe.getBareJidFromJid(jid);
+ var item = this.findItem(from);
+ // not in roster
+ if (!item)
+ {
+ return true;
+ }
+ var type = presence.getAttribute('type');
+ if (type == 'unavailable')
+ {
+ delete item.resources[Strophe.getResourceFromJid(jid)];
+ }
+ else if (!type)
+ {
+ // TODO: add timestamp
+ item.resources[Strophe.getResourceFromJid(jid)] = {
+ show : (presence.getElementsByTagName('show').length !== 0) ? Strophe.getText(presence.getElementsByTagName('show')[0]) : "",
+ status : (presence.getElementsByTagName('status').length !== 0) ? Strophe.getText(presence.getElementsByTagName('status')[0]) : "",
+ priority : (presence.getElementsByTagName('priority').length !== 0) ? Strophe.getText(presence.getElementsByTagName('priority')[0]) : ""
+ };
+ }
+ else
+ {
+ // Stanza is not a presence notification. (It's probably a subscription type stanza.)
+ return true;
+ }
+ this._call_backs(this.items, item);
+ return true;
+ },
+ /** PrivateFunction: _call_backs
+ *
+ */
+ _call_backs : function(items, item)
+ {
+ for (var i = 0; i < this._callbacks.length; i++) // [].forEach my love ...
+ {
+ this._callbacks[i](items, item);
+ }
+ },
+ /** PrivateFunction: _onReceiveIQ
+ * Handle roster push.
+ */
+ _onReceiveIQ : function(iq)
+ {
+ var id = iq.getAttribute('id');
+ var from = iq.getAttribute('from');
+ // Receiving client MUST ignore stanza unless it has no from or from = user's JID.
+ if (from && from !== "" && from != this._connection.jid && from != Strophe.getBareJidFromJid(this._connection.jid))
+ return true;
+ var iqresult = $iq({type: 'result', id: id, from: this._connection.jid});
+ this._connection.send(iqresult);
+ this._updateItems(iq);
+ return true;
+ },
+ /** PrivateFunction: _updateItems
+ * Update items from iq
+ */
+ _updateItems : function(iq)
+ {
+ var query = iq.getElementsByTagName('query');
+ if (query.length !== 0)
+ {
+ this.ver = query.item(0).getAttribute('ver');
+ var self = this;
+ Strophe.forEachChild(query.item(0), 'item',
+ function (item)
+ {
+ self._updateItem(item);
+ }
+ );
+ }
+ this._call_backs(this.items);
+ },
+ /** PrivateFunction: _updateItem
+ * Update internal representation of roster item
+ */
+ _updateItem : function(item)
+ {
+ var jid = item.getAttribute("jid");
+ var name = item.getAttribute("name");
+ var subscription = item.getAttribute("subscription");
+ var ask = item.getAttribute("ask");
+ var groups = [];
+ Strophe.forEachChild(item, 'group',
+ function(group)
+ {
+ groups.push(Strophe.getText(group));
+ }
+ );
+
+ if (subscription == "remove")
+ {
+ this.removeItem(jid);
+ return;
+ }
+
+ item = this.findItem(jid);
+ if (!item)
+ {
+ this.items.push({
+ name : name,
+ jid : jid,
+ subscription : subscription,
+ ask : ask,
+ groups : groups,
+ resources : {}
+ });
+ }
+ else
+ {
+ item.name = name;
+ item.subscription = subscription;
+ item.ask = ask;
+ item.groups = groups;
+ }
+ }
+});
diff --git a/src/templates/roster.html b/src/templates/roster.html
index b3e44979e..2f015b6c3 100644
--- a/src/templates/roster.html
+++ b/src/templates/roster.html
@@ -1,6 +1,5 @@
-
-