From 5e4d6bd3b29e795fc4a0b42423567eaf6db7beae Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 24 Oct 2014 18:58:42 +0200 Subject: [PATCH 01/23] Initial work on adding profiling tests. --- main.js | 1 + spec/controlbox.js | 47 +++++++++++++++++++++++++++++++++++++ spec/profiling.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++ tests/main.js | 8 +++---- tests/mock.js | 38 +++++++++++++++++++++++++----- tests/utils.js | 11 +++++++++ 6 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 spec/profiling.js diff --git a/main.js b/main.js index adca6f286..c0e54fe66 100644 --- a/main.js +++ b/main.js @@ -139,6 +139,7 @@ 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'] }, diff --git a/spec/controlbox.js b/spec/controlbox.js index 74bcab738..8948b6328 100644 --- a/spec/controlbox.js +++ b/spec/controlbox.js @@ -385,6 +385,11 @@ }, converse)); it("can be removed by the user", $.proxy(function () { + // XXX + // This tests fails because "remove" function in strophe.roster + // (line 292) gets called and it then tries to actually remove + // the user which is not in the roster... + // We'll perhaps have to first add the user again... _addContacts(); var name = mock.pend_names[0]; var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -764,6 +769,48 @@ // There should now be one less contact expect(this.roster.length).toEqual(mock.req_names.length-1); }, converse)); + + it("are persisted even if other contacts' change their presence ", $.proxy(function() { + /* This is a regression test. + * https://github.com/jcbrand/converse.js/issues/262 + */ + this.rosterview.model.reset(); + spyOn(this.roster, 'clearCache').andCallThrough(); + expect(this.roster.pluck('jid').length).toBe(0); + + var stanza = $pres({from: 'data@enterprise/resource', type: 'subscribe'}); + this.connection._dataRecv(test_utils.createRequest(stanza)); + expect(this.roster.pluck('jid').length).toBe(1); + expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy(); + + // Taken from the spec + // http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3 + stanza = $iq({ + to: this.connection.jid, + type: 'result', + id: 'roster_1' + }).c('query', { + xmlns: 'jabber:iq:roster', + }).c('item', { + jid: 'romeo@example.net', + name: 'Romeo', + subscription:'both' + }).c('group').t('Friends').up().up() + .c('item', { + jid: 'mercutio@example.org', + name: 'Mercutio', + subscription:'from' + }).c('group').t('Friends').up().up() + .c('item', { + jid: 'benvolio@example.org', + name: 'Benvolio', + subscription:'both' + }).c('group').t('Friends'); + this.connection.roster._onReceiveRosterSuccess(null, stanza.tree()); + expect(this.roster.clearCache).toHaveBeenCalled(); + expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy(); + }, converse)); + }, converse)); describe("All Contacts", $.proxy(function () { diff --git a/spec/profiling.js b/spec/profiling.js new file mode 100644 index 000000000..d195ba99c --- /dev/null +++ b/spec/profiling.js @@ -0,0 +1,58 @@ +(function (root, factory) { + define([ + "jquery", + "mock", + "test_utils" + ], function ($, mock, test_utils) { + return factory($, mock, test_utils); + } + ); +} (this, function ($, mock, test_utils) { + describe("Profiling", function() { + var roster; + beforeEach(function() { + roster = converse.connection.roster; + converse.connection._changeConnectStatus(Strophe.Status.CONNECTED); + }); + + it("adds contacts on presence stanza", $.proxy(function() { + spyOn(this.roster, 'clearCache').andCallThrough(); + expect(this.roster.pluck('jid').length).toBe(0); + + var stanza = $pres({from: 'data@enterprise/resource', type: 'subscribe'}); + this.connection._dataRecv(test_utils.createRequest(stanza)); + expect(this.roster.pluck('jid').length).toBe(1); + expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy(); + + // Taken from the spec + // http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3 + stanza = $iq({ + to: this.connection.jid, + type: 'result', + id: 'roster_1' + }).c('query', { + xmlns: 'jabber:iq:roster', + }).c('item', { + jid: 'romeo@example.net', + name: 'Romeo', + subscription:'both' + }).c('group').t('Friends').up().up() + .c('item', { + jid: 'mercutio@example.org', + name: 'Mercutio', + subscription:'from' + }).c('group').t('Friends').up().up() + .c('item', { + jid: 'benvolio@example.org', + name: 'Benvolio', + subscription:'both' + }).c('group').t('Friends'); + this.connection.roster._onReceiveRosterSuccess(null, stanza.tree()); + expect(this.roster.clearCache).toHaveBeenCalled(); + + expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy(); + }, converse)); + + }); + +})); diff --git a/tests/main.js b/tests/main.js index 1821b7cf5..a50b339aa 100644 --- a/tests/main.js +++ b/tests/main.js @@ -40,8 +40,6 @@ require([ window.converse_api = converse; window.localStorage.clear(); window.sessionStorage.clear(); - // XXX: call this to initialize Strophe plugins - new Strophe.Connection('localhost'); converse.initialize({ prebind: false, @@ -49,7 +47,8 @@ require([ auto_subscribe: false, animate: false, connection: mock.mock_connection, - no_trimming: true + no_trimming: true, + debug: true }, function (converse) { window.converse = converse; window.crypto = { @@ -68,7 +67,8 @@ require([ "spec/controlbox", "spec/chatbox", "spec/chatroom", - "spec/minchats" + "spec/minchats", + "spec/profiling" ], function () { // Make sure this callback is only called once. delete converse.callback; diff --git a/tests/mock.js b/tests/mock.js index 213edb797..8e4dc5dab 100644 --- a/tests/mock.js +++ b/tests/mock.js @@ -36,11 +36,37 @@ 'preventDefault': function () {} }; - mock.mock_connection = { - '_proto': {}, - 'connected': true, - 'authenticated': true, - 'mock': true, + mock.mock_connection = function () { + Strophe.Bosh.prototype._processRequest = function () {}; // Don't attempt to send out stanzas + var c = new Strophe.Connection('jasmine tests'); + c.authenticated = true; + c.connected = true; + c.mock = true; + c.jid = 'dummy@localhost/resource'; + c.vcard = { + 'get': function (callback, jid) { + var fullname; + if (!jid) { + jid = 'dummy@localhost'; + fullname = 'Max Mustermann' ; + } else { + var name = jid.split('@')[0].replace(/\./g, ' ').split(' '); + var last = name.length-1; + name[0] = name[0].charAt(0).toUpperCase()+name[0].slice(1); + name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1); + fullname = name.join(' '); + } + var vcard = $iq().c('vCard').c('FN').t(fullname); + callback(vcard.tree()); + } + }; + c._changeConnectStatus(Strophe.Status.CONNECTED); + c.attach(c.jid); + return c; + }(); + + /* + { 'muc': { 'listRooms': function () {}, 'join': function () {}, @@ -49,7 +75,6 @@ 'groupchat': function () {return String((new Date()).getTime()); } }, 'service': 'jasmine tests', - 'jid': 'dummy@localhost', 'addHandler': function (handler, ns, name, type, id, from, options) { return function () {}; }, @@ -87,5 +112,6 @@ 'items': function () {} } }; + */ return mock; })); diff --git a/tests/utils.js b/tests/utils.js index a1c37f9c2..cf9736637 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -8,6 +8,17 @@ }); }(this, function ($, mock) { var utils = {}; + + utils.createRequest = function (iq) { + iq = typeof iq.tree == "function" ? iq.tree() : iq; + var req = new Strophe.Request(iq, function() {}); + req.getResponse = function() { + var env = new Strophe.Builder('env', {type: 'mock'}).tree(); + env.appendChild(iq); + return env; + }; + return req; + }; utils.closeAllChatBoxes = function () { var i, chatbox; From cdb86788a3b330d0bb342798129e7886bea4bc3b Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 24 Oct 2014 21:45:48 +0200 Subject: [PATCH 02/23] Fix tests. In the previous commit, the mock connection object was refactored to use a real Strophe.Connection object. This caused a test in spec/controlbox.js to fail (due to a method that was now no longer mocked). Added a quick workaround (via monkeypatch) for now. --- spec/controlbox.js | 19 +++++++++++++------ spec/profiling.js | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/spec/controlbox.js b/spec/controlbox.js index 8948b6328..ee4a6549d 100644 --- a/spec/controlbox.js +++ b/spec/controlbox.js @@ -385,11 +385,15 @@ }, converse)); it("can be removed by the user", $.proxy(function () { - // XXX - // This tests fails because "remove" function in strophe.roster - // (line 292) gets called and it then tries to actually remove - // the user which is not in the roster... - // We'll perhaps have to first add the user again... + /* FIXME: Monkepatch + * After refactoring the mock connection to use a + * Strophe.Connection object, these tests fail because "remove" + * function in strophe.roster (line 292) gets called and it + * then tries to actually remove the user which is not in the roster... + */ + var old_remove = this.connection.roster.remove; + this.connection.roster.remove = function (jid, callback) { callback(); }; + _addContacts(); var name = mock.pend_names[0]; var jid = name.replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -407,6 +411,9 @@ expect(this.connection.roster.unauthorize).toHaveBeenCalled(); expect(this.rosterview.model.remove).toHaveBeenCalled(); expect(converse.rosterview.$el.find(".pending-contact-name:contains('"+name+"')").length).toEqual(0); + + /* XXX Restore Monkeypatch */ + this.connection.roster.remove = old_remove; }, converse)); it("do not have a header if there aren't any", $.proxy(function () { @@ -822,7 +829,7 @@ }, converse)); it("are saved to, and can be retrieved from, browserStorage", $.proxy(function () { - var new_attrs, old_attrs, attrs, old_roster; + var new_attrs, old_attrs, attrs; var num_contacts = this.roster.length; new_roster = new this.RosterContacts(); // Roster items are yet to be fetched from browserStorage diff --git a/spec/profiling.js b/spec/profiling.js index d195ba99c..0421f37c9 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -15,7 +15,7 @@ converse.connection._changeConnectStatus(Strophe.Status.CONNECTED); }); - it("adds contacts on presence stanza", $.proxy(function() { + xit("adds contacts on presence stanza", $.proxy(function() { spyOn(this.roster, 'clearCache').andCallThrough(); expect(this.roster.pluck('jid').length).toBe(0); From 6088417df70cdfd431310d38816d23df728ce0d8 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 24 Oct 2014 22:24:05 +0200 Subject: [PATCH 03/23] Add an outline to the profiling tests. --- spec/profiling.js | 50 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/spec/profiling.js b/spec/profiling.js index 0421f37c9..78c167e3b 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -15,42 +15,40 @@ converse.connection._changeConnectStatus(Strophe.Status.CONNECTED); }); - xit("adds contacts on presence stanza", $.proxy(function() { + it("adds hundreds of contacts to the roster", $.proxy(function() { + }, converse)); + + it("adds hundreds of contacts to the roster, with roster groups", $.proxy(function() { + // XXX: Try with groups for now (might also add a test without groups) + converse.roster_groups = true; + spyOn(this.roster, 'clearCache').andCallThrough(); expect(this.roster.pluck('jid').length).toBe(0); - var stanza = $pres({from: 'data@enterprise/resource', type: 'subscribe'}); - this.connection._dataRecv(test_utils.createRequest(stanza)); - expect(this.roster.pluck('jid').length).toBe(1); - expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy(); - - // Taken from the spec - // http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3 - stanza = $iq({ + var stanza = $iq({ to: this.connection.jid, type: 'result', id: 'roster_1' }).c('query', { - xmlns: 'jabber:iq:roster', - }).c('item', { - jid: 'romeo@example.net', - name: 'Romeo', - subscription:'both' - }).c('group').t('Friends').up().up() - .c('item', { - jid: 'mercutio@example.org', - name: 'Mercutio', - subscription:'from' - }).c('group').t('Friends').up().up() - .c('item', { - jid: 'benvolio@example.org', - name: 'Benvolio', - subscription:'both' - }).c('group').t('Friends'); + xmlns: 'jabber:iq:roster' + }); + + _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { + var i, prefix = group.toLowerCase(); + for (i=0; i<100; i++) { + stanza = stanza.c('item', { + jid: prefix+i+'@example.net', + subscription:'both' + }).c('group').t('Friends').up().up(); + } + }); this.connection.roster._onReceiveRosterSuccess(null, stanza.tree()); expect(this.roster.clearCache).toHaveBeenCalled(); - expect(_.contains(this.roster.pluck('jid'), 'data@enterprise')).toBeTruthy(); + expect(this.roster.pluck('jid').length).toBe(400); + }, converse)); + + it("contacts in a very large roster change their statuses", $.proxy(function() { }, converse)); }); From f4ff6370b9746613a4dabda2b24059dd8b236190 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 25 Oct 2014 09:57:05 +0200 Subject: [PATCH 04/23] Performance fix. Debounce the update method on the roster. --- converse.js | 4 ++-- spec/profiling.js | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/converse.js b/converse.js index 07593cde2..e0865331d 100644 --- a/converse.js +++ b/converse.js @@ -3851,14 +3851,14 @@ this.model.on("reset", this.reset, this); }, - update: function () { + update: _.debounce(function () { var $count = $('#online-count'); $count.text('('+converse.roster.getNumOnlineContacts()+')'); if (!$count.is(':visible')) { $count.show(); } return this.showHideFilter(); - }, + }, 300), render: function () { this.$el.html(converse.templates.roster({ diff --git a/spec/profiling.js b/spec/profiling.js index 78c167e3b..f7f742bdf 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -50,7 +50,5 @@ it("contacts in a very large roster change their statuses", $.proxy(function() { }, converse)); - }); - })); From 3d32bfefc3fafe0deac102bd392cb5076d0db1aa Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 25 Oct 2014 12:33:08 +0200 Subject: [PATCH 05/23] Don't add test users alphabetically, but rather randomly --- spec/profiling.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/profiling.js b/spec/profiling.js index f7f742bdf..5ed822ea2 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -34,12 +34,12 @@ }); _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { - var i, prefix = group.toLowerCase(); + var i; for (i=0; i<100; i++) { stanza = stanza.c('item', { - jid: prefix+i+'@example.net', + jid: Math.random().toString().replace('0.', '')+'@example.net', subscription:'both' - }).c('group').t('Friends').up().up(); + }).c('group').t(group).up().up(); } }); this.connection.roster._onReceiveRosterSuccess(null, stanza.tree()); From 2f968f7095551c61499b0f63c891ab22f30de2cd Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 25 Oct 2014 12:33:24 +0200 Subject: [PATCH 06/23] More optimizations. Don't sort the global ContactRosters collection (we only need to sort the individual groups). Only add the roster to the DOM once the users have been added. --- converse.js | 18 +++++++++++------- src/templates/roster.html | 1 - 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/converse.js b/converse.js index e0865331d..a0fd66b8b 100644 --- a/converse.js +++ b/converse.js @@ -3356,13 +3356,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; @@ -3503,7 +3503,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 @@ -3849,6 +3849,7 @@ converse.roster.on("remove", this.update, this); this.model.on("add", this.onGroupAdd, this); this.model.on("reset", this.reset, this); + this.$roster = $('
'); }, update: _.debounce(function () { @@ -3857,6 +3858,9 @@ if (!$count.is(':visible')) { $count.show(); } + if (this.$roster.parent().length === 0) { + this.$el.append(this.$roster); + } return this.showHideFilter(); }, 300), @@ -3942,7 +3946,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(); @@ -4047,7 +4051,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); } @@ -4060,7 +4064,7 @@ */ var index = this.model.indexOf(view.model); 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 { diff --git a/src/templates/roster.html b/src/templates/roster.html index b3e44979e..9cb1da749 100644 --- a/src/templates/roster.html +++ b/src/templates/roster.html @@ -3,4 +3,3 @@ -
From 695c8f441d640571307a507820f4f9b3ccabdaca Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 26 Oct 2014 17:10:58 +0100 Subject: [PATCH 07/23] Minimize debounce time for update method to 100ms. --- converse.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/converse.js b/converse.js index a0fd66b8b..da9259ad7 100644 --- a/converse.js +++ b/converse.js @@ -1935,7 +1935,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. + // converse.roster.fetch() converse.rosterview.render().fetch().update(); + // TODO: See if we can optimize here by not calling this method + // on every page load. converse.connection.roster.get(function () {}); return this; }, @@ -3861,8 +3870,9 @@ if (this.$roster.parent().length === 0) { this.$el.append(this.$roster); } + console.log('update called'); return this.showHideFilter(); - }, 300), + }, 100), render: function () { this.$el.html(converse.templates.roster({ From 015367af6242bb104f6ba7c6b56ec15a2460eaf1 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 26 Oct 2014 17:12:59 +0100 Subject: [PATCH 08/23] Add another profiling method for adding users without groups --- spec/profiling.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/spec/profiling.js b/spec/profiling.js index 5ed822ea2..3ebe61463 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -9,22 +9,15 @@ ); } (this, function ($, mock, test_utils) { describe("Profiling", function() { - var roster; beforeEach(function() { - roster = converse.connection.roster; + converse.connection.roster.items = []; converse.connection._changeConnectStatus(Strophe.Status.CONNECTED); }); it("adds hundreds of contacts to the roster", $.proxy(function() { - }, converse)); - - it("adds hundreds of contacts to the roster, with roster groups", $.proxy(function() { - // XXX: Try with groups for now (might also add a test without groups) - converse.roster_groups = true; - + converse.roster_groups = false; spyOn(this.roster, 'clearCache').andCallThrough(); expect(this.roster.pluck('jid').length).toBe(0); - var stanza = $iq({ to: this.connection.jid, type: 'result', @@ -32,7 +25,6 @@ }).c('query', { xmlns: 'jabber:iq:roster' }); - _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { var i; for (i=0; i<100; i++) { @@ -44,7 +36,31 @@ }); this.connection.roster._onReceiveRosterSuccess(null, stanza.tree()); expect(this.roster.clearCache).toHaveBeenCalled(); + expect(this.roster.pluck('jid').length).toBe(400); + }, converse)); + it("adds hundreds of contacts to the roster, with roster groups", $.proxy(function() { + converse.roster_groups = true; + spyOn(this.roster, 'clearCache').andCallThrough(); + expect(this.roster.pluck('jid').length).toBe(0); + var stanza = $iq({ + to: this.connection.jid, + type: 'result', + id: 'roster_1' + }).c('query', { + xmlns: 'jabber:iq:roster' + }); + _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { + var i; + for (i=0; i<100; i++) { + stanza = stanza.c('item', { + jid: Math.random().toString().replace('0.', '')+'@example.net', + subscription:'both' + }).c('group').t(group).up().up(); + } + }); + this.connection.roster._onReceiveRosterSuccess(null, stanza.tree()); + expect(this.roster.clearCache).toHaveBeenCalled(); expect(this.roster.pluck('jid').length).toBe(400); }, converse)); From 07186bcecd9428837aa6f01a4029c2ef91c3ca9c Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 26 Oct 2014 20:20:05 +0100 Subject: [PATCH 09/23] Performance fix. Set display "none" on the roster element. --- converse.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/converse.js b/converse.js index da9259ad7..4ea2072d0 100644 --- a/converse.js +++ b/converse.js @@ -1941,6 +1941,8 @@ // 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(); // TODO: See if we can optimize here by not calling this method @@ -3858,7 +3860,7 @@ converse.roster.on("remove", this.update, this); this.model.on("add", this.onGroupAdd, this); this.model.on("reset", this.reset, this); - this.$roster = $('
'); + this.$roster = $(''); }, update: _.debounce(function () { @@ -3868,7 +3870,7 @@ $count.show(); } if (this.$roster.parent().length === 0) { - this.$el.append(this.$roster); + this.$el.append(this.$roster.show()); } console.log('update called'); return this.showHideFilter(); From 902e833dec24afb5ac9b3f908d0de0141d4cb81f Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 26 Oct 2014 23:10:43 +0100 Subject: [PATCH 10/23] Some refactoring. Add showInRoster method which checks if a contact should appear in the roster (depends on show_only_online_users setting) --- converse.js | 41 ++++++++++++++++++++++------------------- spec/profiling.js | 5 +++-- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/converse.js b/converse.js index 4ea2072d0..332043b1c 100644 --- a/converse.js +++ b/converse.js @@ -3223,6 +3223,10 @@ 'status': '' }, attributes); this.set(attrs); + }, + + showInRoster: function () { + return (!converse.show_only_online_users || this.get('chat_status') === 'online'); } }); @@ -3237,24 +3241,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 @@ -3304,6 +3296,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'), @@ -3697,11 +3695,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(); } + } } }, @@ -3723,6 +3723,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(); }, @@ -3740,7 +3743,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)); diff --git a/spec/profiling.js b/spec/profiling.js index 3ebe61463..22c8659a7 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -40,6 +40,7 @@ }, converse)); it("adds hundreds of contacts to the roster, with roster groups", $.proxy(function() { + // converse.show_only_online_users = true; converse.roster_groups = true; spyOn(this.roster, 'clearCache').andCallThrough(); expect(this.roster.pluck('jid').length).toBe(0); @@ -52,7 +53,7 @@ }); _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { var i; - for (i=0; i<100; i++) { + for (i=0; i<500; i++) { stanza = stanza.c('item', { jid: Math.random().toString().replace('0.', '')+'@example.net', subscription:'both' @@ -61,7 +62,7 @@ }); this.connection.roster._onReceiveRosterSuccess(null, stanza.tree()); expect(this.roster.clearCache).toHaveBeenCalled(); - expect(this.roster.pluck('jid').length).toBe(400); + //expect(this.roster.pluck('jid').length).toBe(400); }, converse)); it("contacts in a very large roster change their statuses", $.proxy(function() { From d03a9a019229c6896688e504cbd9179a7b5899bd Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 26 Oct 2014 23:11:58 +0100 Subject: [PATCH 11/23] bugfix in positionGroup. Index must always be 0 if there aren't any group elements in the dom yet. --- converse.js | 5 +++-- spec/profiling.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/converse.js b/converse.js index 332043b1c..749bb8758 100644 --- a/converse.js +++ b/converse.js @@ -4077,13 +4077,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-group'), + index = $groups.length ? this.model.indexOf(view.model) : 0; if (index === 0) { 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; }, diff --git a/spec/profiling.js b/spec/profiling.js index 22c8659a7..7e082f901 100644 --- a/spec/profiling.js +++ b/spec/profiling.js @@ -53,7 +53,7 @@ }); _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { var i; - for (i=0; i<500; i++) { + for (i=0; i<100; i++) { stanza = stanza.c('item', { jid: Math.random().toString().replace('0.', '')+'@example.net', subscription:'both' From 8998a057dab5edfb6fd869078dfb48d03b550601 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 27 Oct 2014 18:48:54 +0100 Subject: [PATCH 12/23] Make sure that subscribeToRosterSuggestions is ... called sequentially with enough time between each call. --- converse.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/converse.js b/converse.js index 749bb8758..96bf3ce6d 100644 --- a/converse.js +++ b/converse.js @@ -1935,7 +1935,7 @@ b64_sha1('converse.roster.groups'+converse.bare_jid)); converse.rosterview = new converse.RosterView({model: rostergroups}); this.contactspanel.$el.append(converse.rosterview.$el); - // TODO: + // 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. @@ -3379,15 +3379,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; @@ -3725,7 +3723,7 @@ 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 + // showInRoster this.$el.nextUntil('dt').addBack().show(); }, @@ -3988,8 +3986,19 @@ }, 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); }, From ec00a04068e0fd409c10f875f3febbac989a9b41 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 27 Oct 2014 21:35:06 +0100 Subject: [PATCH 13/23] Bugfix. .roster-group elements must be found relative to --- converse.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/converse.js b/converse.js index 96bf3ce6d..bf258c8b6 100644 --- a/converse.js +++ b/converse.js @@ -3981,8 +3981,8 @@ 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 () { @@ -4086,7 +4086,7 @@ /* Place the group's DOM element in the correct alphabetical * position amongst the other groups in the roster. */ - var $groups = this.$('.roster-group'), + var $groups = this.$roster.find('.roster-group'), index = $groups.length ? this.model.indexOf(view.model) : 0; if (index === 0) { this.$roster.prepend(view.$el); @@ -4101,7 +4101,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); From c2063eb1a71a936935780ea5c7f1bed309a58442 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 27 Oct 2014 21:35:25 +0100 Subject: [PATCH 14/23] Keep on getting disconnected. This fixed it. Seems like the RID gets incremented once too many (outside of upper bound of expected window), and then server responds with item-not-found. See: http://xmpp.org/extensions/xep-0124.html --- converse.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/converse.js b/converse.js index bf258c8b6..e72e2454e 100644 --- a/converse.js +++ b/converse.js @@ -4592,8 +4592,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) { From b2b4474439017ca3b4c7e053b50b7eea19e78d68 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 27 Oct 2014 21:41:41 +0100 Subject: [PATCH 15/23] Performance fix. Don't loop through whole roster when only one item changed. updates #151 --- converse.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/converse.js b/converse.js index e72e2454e..8434a3978 100644 --- a/converse.js +++ b/converse.js @@ -3493,7 +3493,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) { From 37eefa67d0f96b80689d971450a19a1bcb91dbd3 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 27 Oct 2014 21:53:05 +0100 Subject: [PATCH 16/23] Performance fix. Don't query for the roster on each page load. Instead, just populate the roster from sessionStorage if available. --- converse.js | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/converse.js b/converse.js index 8434a3978..044085a62 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}); @@ -1945,9 +1946,6 @@ // each item add... // converse.roster.fetch() converse.rosterview.render().fetch().update(); - // TODO: See if we can optimize here by not calling this method - // on every page load. - converse.connection.roster.get(function () {}); return this; }, @@ -3889,10 +3887,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; }, From 7e4c1d6d8d75460aa8faffa4af7abccb8b315247 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 27 Oct 2014 21:54:00 +0100 Subject: [PATCH 17/23] Keep a local customized copy of strophe.roster.js The new changes made to strophe.roster.js are incompatible with the way converse.js works. Will likely replace strophe.roster.js completely. --- bower.json | 3 +- main.js | 2 +- src/strophe.roster.js | 447 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 src/strophe.roster.js diff --git a/bower.json b/bower.json index 594e1c7bf..efac268a7 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": "git@github.com:strophe/strophejs-plugins.git#a56421ff4ecf0807113ab48c46728715597df599" }, "exportsOverride": {} } diff --git a/main.js b/main.js index c0e54fe66..0946c251f 100644 --- a/main.js +++ b/main.js @@ -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', diff --git a/src/strophe.roster.js b/src/strophe.roster.js new file mode 100644 index 000000000..79182d118 --- /dev/null +++ b/src/strophe.roster.js @@ -0,0 +1,447 @@ +/* + Copyright 2010, François de Metz +*/ +/** + * 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; + } + } +}); From 55e32c735d48bd1d0146313232549ec508107954 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 27 Oct 2014 23:06:11 +0100 Subject: [PATCH 18/23] Fix tests --- converse.js | 3 +- spec/chatbox.js | 53 +-- spec/controlbox.js | 657 ++++++++++++++++++++++---------------- spec/profiling.js | 4 +- src/templates/roster.html | 4 +- 5 files changed, 422 insertions(+), 299 deletions(-) diff --git a/converse.js b/converse.js index 044085a62..7031352d5 100644 --- a/converse.js +++ b/converse.js @@ -3874,7 +3874,7 @@ } console.log('update called'); return this.showHideFilter(); - }, 100), + }, converse.animate ? 100 : 0), render: function () { this.$el.html(converse.templates.roster({ @@ -4010,6 +4010,7 @@ reset: function () { converse.roster.reset(); this.removeAll(); + this.$roster = $(''); this.render().update(); return this; }, 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