From 5a5d3757ea2725caf14822ee7b2a83d951e341cf Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 10 Jul 2015 15:02:48 +0200 Subject: [PATCH 01/45] Move the addEmoticons jQuery plugin to utils.js --- converse.js | 35 ----------------------------------- src/utils.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/converse.js b/converse.js index 095b433cf..d43d7075c 100644 --- a/converse.js +++ b/converse.js @@ -90,41 +90,6 @@ return [components.shift(), components.join(delimiter)]; }; - $.fn.addEmoticons = function () { - if (converse.visible_toolbar_buttons.emoticons) { - if (this.length > 0) { - this.each(function (i, obj) { - var text = $(obj).html(); - text = text.replace(/>:\)/g, ''); - text = text.replace(/:\)/g, ''); - text = text.replace(/:\-\)/g, ''); - text = text.replace(/;\)/g, ''); - text = text.replace(/;\-\)/g, ''); - text = text.replace(/:D/g, ''); - text = text.replace(/:\-D/g, ''); - text = text.replace(/:P/g, ''); - text = text.replace(/:\-P/g, ''); - text = text.replace(/:p/g, ''); - text = text.replace(/:\-p/g, ''); - text = text.replace(/8\)/g, ''); - text = text.replace(/:S/g, ''); - text = text.replace(/:\\/g, ''); - text = text.replace(/:\/ /g, ''); - text = text.replace(/>:\(/g, ''); - text = text.replace(/:\(/g, ''); - text = text.replace(/:\-\(/g, ''); - text = text.replace(/:O/g, ''); - text = text.replace(/:\-O/g, ''); - text = text.replace(/\=\-O/g, ''); - text = text.replace(/\(\^.\^\)b/g, ''); - text = text.replace(/<3/g, ''); - $(obj).html(text); - }); - } - } - return this; - }; - var converse = { plugins: {}, templates: templates, diff --git a/src/utils.js b/src/utils.js index ef2a1b8ad..fb0d90194 100644 --- a/src/utils.js +++ b/src/utils.js @@ -50,6 +50,41 @@ return this; }; + $.fn.addEmoticons = function () { + if (converse.visible_toolbar_buttons.emoticons) { + if (this.length > 0) { + this.each(function (i, obj) { + var text = $(obj).html(); + text = text.replace(/>:\)/g, ''); + text = text.replace(/:\)/g, ''); + text = text.replace(/:\-\)/g, ''); + text = text.replace(/;\)/g, ''); + text = text.replace(/;\-\)/g, ''); + text = text.replace(/:D/g, ''); + text = text.replace(/:\-D/g, ''); + text = text.replace(/:P/g, ''); + text = text.replace(/:\-P/g, ''); + text = text.replace(/:p/g, ''); + text = text.replace(/:\-p/g, ''); + text = text.replace(/8\)/g, ''); + text = text.replace(/:S/g, ''); + text = text.replace(/:\\/g, ''); + text = text.replace(/:\/ /g, ''); + text = text.replace(/>:\(/g, ''); + text = text.replace(/:\(/g, ''); + text = text.replace(/:\-\(/g, ''); + text = text.replace(/:O/g, ''); + text = text.replace(/:\-O/g, ''); + text = text.replace(/\=\-O/g, ''); + text = text.replace(/\(\^.\^\)b/g, ''); + text = text.replace(/<3/g, ''); + $(obj).html(text); + }); + } + } + return this; + }; + var utils = { // Translation machinery // --------------------- From 2442eb74f47e6fed298e932a35fae2f368da73d0 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 10 Jul 2015 15:07:32 +0200 Subject: [PATCH 02/45] Use Strophe.NS and only advertise enabled features. --- converse.js | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/converse.js b/converse.js index d43d7075c..88a0e66cd 100644 --- a/converse.js +++ b/converse.js @@ -149,7 +149,10 @@ Strophe.error = function (msg) { converse.log(msg, 'error'); }; // Add Strophe Namespaces + Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2'); Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); + Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); + Strophe.addNamespace('MAM', 'urn:xmpp:mam:0'); Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin"); Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner"); Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register"); @@ -158,7 +161,6 @@ Strophe.addNamespace('REGISTER', 'jabber:iq:register'); Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); Strophe.addNamespace('XFORM', 'jabber:x:data'); - Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); // Add Strophe Statuses var i = 0; @@ -839,7 +841,7 @@ id: 'enablecarbons', type: 'set' }) - .c('enable', {xmlns: 'urn:xmpp:carbons:2'}); + .c('enable', {xmlns: Strophe.NS.CARBONS}); this.connection.addHandler(function (iq) { if ($(iq).find('error').length > 0) { converse.log('ERROR: An error occured while trying to enable message carbons.'); @@ -1367,7 +1369,7 @@ if (this.model.get('otr_status') != UNENCRYPTED) { // OTR messages aren't carbon copied - message.c('private', {'xmlns': 'urn:xmpp:carbons:2'}); + message.c('private', {'xmlns': Strophe.NS.CARBONS}); } converse.connection.send(message); if (converse.forward_messages) { @@ -5061,19 +5063,23 @@ * it will advertise to any #info queries made to it. * * See: http://xmpp.org/extensions/xep-0030.html#info - * - * TODO: these features need to be added in the relevant - * feature-providing Models, not here */ - converse.connection.disco.addFeature(Strophe.NS.CHATSTATES); - converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support - converse.connection.disco.addFeature('jabber:x:conference'); - converse.connection.disco.addFeature('urn:xmpp:carbons:2'); - converse.connection.disco.addFeature(Strophe.NS.VCARD); - converse.connection.disco.addFeature(Strophe.NS.BOSH); - converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO); - converse.connection.disco.addFeature(Strophe.NS.MUC); - return this; + converse.connection.disco.addFeature('jabber:x:conference'); + converse.connection.disco.addFeature(Strophe.NS.BOSH); + converse.connection.disco.addFeature(Strophe.NS.CHATSTATES); + converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO); + converse.connection.disco.addFeature(Strophe.NS.MAM); + converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support + if (converse.use_vcards) { + converse.connection.disco.addFeature(Strophe.NS.VCARD); + } + if (converse.allow_muc) { + converse.connection.disco.addFeature(Strophe.NS.MUC); + } + if (converse.message_carbons) { + converse.connection.disco.addFeature(Strophe.NS.CARBONS); + } + return this; }, onItems: function (stanza) { From eeeaddbe047b0b5424540dc402e8ddc4ecace249 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 10 Jul 2015 15:07:53 +0200 Subject: [PATCH 03/45] Add the ability to read and set MAM preferences. --- converse.js | 54 +++++++++++++++++ docs/source/configuration.rst | 13 +++++ spec/mam.js | 106 ++++++++++++++++++++++++++++++++++ tests/main.js | 1 + 4 files changed, 174 insertions(+) create mode 100644 spec/mam.js diff --git a/converse.js b/converse.js index 88a0e66cd..cd5fe8c03 100644 --- a/converse.js +++ b/converse.js @@ -313,6 +313,7 @@ hide_offline_users: false, jid: undefined, keepalive: false, + message_archiving: 'never', // Supported values are 'always', 'never', 'roster' (See https://xmpp.org/extensions/xep-0313.html#prefs ) message_carbons: false, // Support for XEP-280 no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width) ping_interval: 180, //in seconds @@ -5041,6 +5042,7 @@ this.addClientIdentities().addClientFeatures(); this.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.features'+converse.bare_jid)); + this.on('add', this.onFeatureAdded, this); if (this.browserStorage.records.length === 0) { // browserStorage is empty, so we've likely never queried this // domain for features yet @@ -5051,6 +5053,58 @@ } }, + onFeatureAdded: function (feature) { + if (feature.get('var') == Strophe.NS.MAM) { + // Ask the server for archiving preferences + converse.connection.sendIQ( + $iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}), + _.bind(this.onMAMPreferences, this, feature), + _.bind(this.onMAMError, this, feature) + ); + } + }, + + onMAMPreferences: function (feature, iq) { + /* Handle returned IQ stanza containing Message Archive + * Management (XEP-0313) preferences. + * + * XXX: For now we only handle the global default preference. + * The XEP also provides for per-JID preferences, which is + * currently not supported in converse.js. + * + * Per JID preferences will be set in chat boxes, so it'll + * probbaly be handled elsewhere in any case. + */ + var $prefs = $(iq).find('prefs[xmlns="'+Strophe.NS.MAM+'"]'); + var default_pref = $prefs.attr('default'); + var stanza; + if (default_pref !== converse.message_archiving) { + stanza = $iq({'type': 'set'}).c('prefs', {'xmlns':Strophe.NS.MAM, 'default':converse.message_archiving}); + $prefs.children().each(function (idx, child) { + stanza.cnode(child).up(); + }); + converse.connection.sendIQ(stanza, _.bind(function (feature, iq) { + // XXX: Strictly speaking, the server should respond with the updated prefs + // (see example 18: https://xmpp.org/extensions/xep-0313.html#config) + // but Prosody doesn't do this, so we don't rely on it. + feature.save({'preferences': {'default':converse.message_archiving}}); + }, this, feature), + _.bind(this.onMAMError, this, feature) + ); + } else { + feature.save({'preferences': {'default':converse.message_archiving}}); + } + }, + + onMAMError: function (iq) { + if ($(iq).find('feature-not-implemented').length) { + converse.log("Message Archive Management (XEP-0313) not supported by this browser"); + } else { + converse.log("An error occured while trying to set archiving preferences."); + converse.log(iq); + } + }, + addClientIdentities: function () { /* See http://xmpp.org/registrar/disco-categories.html */ diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 3cc591a1b..1d1008a6a 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -327,6 +327,19 @@ See also: `XEP-0198 `_, specifically with regards to "stream resumption". + +message_archives +---------------- + +* Default: ``never`` + +Provides support for `XEP-0313: Message Archive Management `_ + +This sets the default archiving preference. Valid values are ``never``, ``always`` and ``roster``. + +``roster`` means that only messages to and from JIDs in your roster will be +archived. The other two values are self-explanatory. + message_carbons --------------- diff --git a/spec/mam.js b/spec/mam.js new file mode 100644 index 000000000..343916ff8 --- /dev/null +++ b/spec/mam.js @@ -0,0 +1,106 @@ +(function (root, factory) { + define([ + "jquery", + "mock", + "test_utils" + ], function ($, mock, test_utils) { + return factory($, mock, test_utils); + } + ); +} (this, function ($, mock, test_utils) { + "use strict"; + var Strophe = converse_api.env.Strophe; + var $iq = converse_api.env.$iq; + var $pres = converse_api.env.$pres; + // See: + // https://xmpp.org/rfcs/rfc3921.html + + describe("Message Archive Management", $.proxy(function (mock, test_utils) { + // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config + + describe("The default preference", $.proxy(function (mock, test_utils) { + beforeEach(function () { + test_utils.closeAllChatBoxes(); + test_utils.removeControlBox(); + converse.roster.browserStorage._clear(); + test_utils.initConverse(); + test_utils.openControlBox(); + test_utils.openContactsPanel(); + }); + + it("is set once server support for MAM has been confirmed", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + spyOn(converse.features, 'onMAMPreferences').andCallThrough(); + + var feature = new converse.Feature({ + 'var': Strophe.NS.MAM + }); + spyOn(feature, 'save').andCallFake(feature.set); // Save will complain about a url not being set + converse.features.onFeatureAdded(feature); + + expect(converse.connection.sendIQ).toHaveBeenCalled(); + expect(sent_stanza.toLocaleString()).toBe( + ""+ + ""+ + "" + ); + + converse.message_archiving = 'never'; + /* Example 15. Server responds with current preferences + * + * + * + * + * + * + * + */ + var stanza = $iq({'type': 'result', 'id': IQ_id}) + .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'}) + .c('always').c('jid').t('romeo@montague.lit').up().up() + .c('never').c('jid').t('montague@montague.lit'); + converse.connection._dataRecv(test_utils.createRequest(stanza)); + + expect(converse.features.onMAMPreferences).toHaveBeenCalled(); + + expect(converse.connection.sendIQ.callCount).toBe(2); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + "romeo@montague.lit"+ + "montague@montague.lit"+ + ""+ + "" + ); + + expect(feature.get('preference')).toBe(undefined); + /* + * + * + * romeo@montague.lit + * + * + * montague@montague.lit + * + * + * + */ + stanza = $iq({'type': 'result', 'id': IQ_id}) + .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'}) + .c('always').up() + .c('never').up(); + converse.connection._dataRecv(test_utils.createRequest(stanza)); + expect(feature.save).toHaveBeenCalled(); + expect(feature.get('preferences').default).toBe('never'); + + // Restore + converse.message_archiving = 'never'; + }); + }, converse, mock, test_utils)); + }, converse, mock, test_utils)); +})); diff --git a/tests/main.js b/tests/main.js index 827f989e7..ce3d52d40 100644 --- a/tests/main.js +++ b/tests/main.js @@ -61,6 +61,7 @@ require([ "console-runner", "spec/converse", "spec/protocol", + "spec/mam", "spec/otr", "spec/eventemitter", "spec/controlbox", From b77d76b3642bd65d1a80205365c527dd892f6e5f Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 10 Jul 2015 17:26:54 +0200 Subject: [PATCH 04/45] Started adding the API for querying archived messages. --- converse.js | 42 ++++++++++ main.js | 2 +- spec/mam.js | 223 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 256 insertions(+), 11 deletions(-) diff --git a/converse.js b/converse.js index cd5fe8c03..8db580d5c 100644 --- a/converse.js +++ b/converse.js @@ -160,6 +160,7 @@ Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user"); Strophe.addNamespace('REGISTER', 'jabber:iq:register'); Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); + Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); Strophe.addNamespace('XFORM', 'jabber:x:data'); // Add Strophe Statuses @@ -6106,6 +6107,47 @@ return _.map(jids, getWrappedChatBox); } }, + 'archive': { + 'query': function (options, callback, errback) { + var date; + // Available options are jid, limit, start, end, after, before + if (typeof options == "function") { + callback = options; + errback = callback; + } + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + throw new Error('This server does not support XEP-0313, Message Archive Management'); + } + var stanza = $iq({'type':'set'}).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':converse.connection.getUniqueId()}); + if (typeof options != "undefined") { + stanza.c('x', {'xmlns':'jabber:x:data'}) + .c('field', {'var':'FORM_TYPE'}) + .c('value').t(Strophe.NS.MAM).up().up(); + + if (options.jid) { + stanza.c('field', {'var':'with'}).c('value').t(options.jid).up().up(); + } + _.each(['start', 'end'], function (t) { + if (options[t]) { + date = moment(options[t]); + if (date.isValid()) { + stanza.c('field', {'var':t}).c('value').t(date.format()).up().up(); + } else { + throw new TypeError('archive.query: invalid date provided for: '+t); + } + } + }); + stanza.up(); + if (options.limit) { + stanza.c('set', {'xmlns':Strophe.NS.RSM}).c('max').t(options.limit).up(); + } + if (options.after) { + stanza.c('after').t(options.after).up(); + } + } + converse.connection.sendIQ(stanza, callback, errback); + } + }, 'rooms': { 'open': function (jids, nick) { if (!nick) { diff --git a/main.js b/main.js index 0797a2f8f..c31ca68e2 100644 --- a/main.js +++ b/main.js @@ -26,7 +26,7 @@ require.config({ "jquery-private": "src/jquery-private", "jquery.browser": "components/jquery.browser/dist/jquery.browser", "jquery.easing": "components/jquery-easing-original/index", // XXX: Only required for https://conversejs.org website - "moment": "components/momentjs/min/moment.min", + "moment": "components/momentjs/moment", "strophe-base64": "components/strophejs/src/base64", "strophe-bosh": "components/strophejs/src/bosh", "strophe-core": "components/strophejs/src/core", diff --git a/spec/mam.js b/spec/mam.js index 343916ff8..11928f7b1 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -12,22 +12,225 @@ var Strophe = converse_api.env.Strophe; var $iq = converse_api.env.$iq; var $pres = converse_api.env.$pres; - // See: - // https://xmpp.org/rfcs/rfc3921.html + // See: https://xmpp.org/rfcs/rfc3921.html describe("Message Archive Management", $.proxy(function (mock, test_utils) { // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config - describe("The default preference", $.proxy(function (mock, test_utils) { - beforeEach(function () { - test_utils.closeAllChatBoxes(); - test_utils.removeControlBox(); - converse.roster.browserStorage._clear(); - test_utils.initConverse(); - test_utils.openControlBox(); - test_utils.openContactsPanel(); + describe("The archive.query API", $.proxy(function (mock, test_utils) { + + it("can be used to query for all archived messages", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + converse_api.archive.query(); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""); }); + it("can be used to query for all messages to/from a particular JID", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + converse_api.archive.query({'jid':'juliet@capulet.lit'}); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + ""+ + ""+ + "urn:xmpp:mam:0"+ + ""+ + ""+ + "juliet@capulet.lit"+ + ""+ + ""+ + ""+ + "" + ); + }); + + it("can be used to query for all messages in a certain timespan", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + // Mock the browser's method for returning the timezone + var getTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function () { + return -120; + }; + converse_api.archive.query({ + 'start': '2010-06-07T00:00:00Z', + 'end': '2010-07-07T13:23:54Z' + + }); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + ""+ + ""+ + "urn:xmpp:mam:0"+ + ""+ + ""+ + "2010-06-07T02:00:00+02:00"+ + ""+ + ""+ + "2010-07-07T15:23:54+02:00"+ + ""+ + ""+ + ""+ + "" + ); + // Restore + Date.prototype.getTimezoneOffset = getTimezoneOffset; + }); + + it("throws a TypeError if an invalid date is provided", function () { + expect(_.partial(converse_api.archive.query, {'start': 'not a real date'})).toThrow( + new TypeError('archive.query: invalid date provided for: start') + ); + }); + + it("can be used to query for all messages after a certain time", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + // Mock the browser's method for returning the timezone + var getTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function () { + return -120; + }; + converse_api.archive.query({'start': '2010-06-07T00:00:00Z'}); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + ""+ + ""+ + "urn:xmpp:mam:0"+ + ""+ + ""+ + "2010-06-07T02:00:00+02:00"+ + ""+ + ""+ + ""+ + "" + ); + // Restore + Date.prototype.getTimezoneOffset = getTimezoneOffset; + }); + + it("can be used to query for a limited set of results", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + // Mock the browser's method for returning the timezone + var getTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function () { + return -120; + }; + converse_api.archive.query({'start': '2010-06-07T00:00:00Z', 'limit':10}); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + ""+ + ""+ + "urn:xmpp:mam:0"+ + ""+ + ""+ + "2010-06-07T02:00:00+02:00"+ + ""+ + ""+ + ""+ + "10"+ + ""+ + ""+ + "" + ); + // Restore + Date.prototype.getTimezoneOffset = getTimezoneOffset; + }); + + it("can be used to page through results", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + // Mock the browser's method for returning the timezone + var getTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function () { + return -120; + }; + converse_api.archive.query({ + 'start': '2010-06-07T00:00:00Z', + 'after': '09af3-cc343-b409f', + 'limit':10 + }); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + ""+ + ""+ + "urn:xmpp:mam:0"+ + ""+ + ""+ + "2010-06-07T02:00:00+02:00"+ + ""+ + ""+ + ""+ + "10"+ + "09af3-cc343-b409f"+ + ""+ + ""+ + "" + ); + // Restore + Date.prototype.getTimezoneOffset = getTimezoneOffset; + }); + + }, converse, mock, test_utils)); + + describe("The default preference", $.proxy(function (mock, test_utils) { + it("is set once server support for MAM has been confirmed", function () { var sent_stanza, IQ_id; var sendIQ = converse.connection.sendIQ; From 1261a2525d3240d0e9a9ccba97377be805a2dcee Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 11 Jul 2015 12:03:20 +0200 Subject: [PATCH 05/45] Use Strophe.RSM for result set management. --- converse.js | 17 +++++++++++------ main.js | 12 ++++++------ spec/mam.js | 4 ++-- src/deps-full.js | 3 ++- src/deps-no-otr.js | 3 ++- src/deps-website-no-otr.js | 3 ++- src/deps-website.js | 3 ++- 7 files changed, 27 insertions(+), 18 deletions(-) diff --git a/converse.js b/converse.js index 8db580d5c..bcff4a19c 100644 --- a/converse.js +++ b/converse.js @@ -118,6 +118,11 @@ } }; + // Global constants + + // XEP-0059 Result Set Management + var RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count']; + var STATUS_WEIGHTS = { 'offline': 6, 'unavailable': 5, @@ -6138,14 +6143,14 @@ } }); stanza.up(); - if (options.limit) { - stanza.c('set', {'xmlns':Strophe.NS.RSM}).c('max').t(options.limit).up(); - } - if (options.after) { - stanza.c('after').t(options.after).up(); + if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) { + stanza.cnode(new Strophe.RSM(options).toXML()); } } - converse.connection.sendIQ(stanza, callback, errback); + converse.connection.sendIQ(stanza, function (iq) { + var rsm = new Strophe.RSM({xml: iq.getElementsByTagName('set')[0]}); + return _.bind(callback, this, arguments)(rsm); + }, errback); } }, 'rooms': { diff --git a/main.js b/main.js index c31ca68e2..a4d1bb1a5 100644 --- a/main.js +++ b/main.js @@ -27,17 +27,18 @@ require.config({ "jquery.browser": "components/jquery.browser/dist/jquery.browser", "jquery.easing": "components/jquery-easing-original/index", // XXX: Only required for https://conversejs.org website "moment": "components/momentjs/moment", + "strophe": "components/strophejs/src/wrapper", "strophe-base64": "components/strophejs/src/base64", "strophe-bosh": "components/strophejs/src/bosh", "strophe-core": "components/strophejs/src/core", - "strophe": "components/strophejs/src/wrapper", "strophe-md5": "components/strophejs/src/md5", + "strophe-polyfill": "components/strophejs/src/polyfills", "strophe-sha1": "components/strophejs/src/sha1", "strophe-websocket": "components/strophejs/src/websocket", - "strophe-polyfill": "components/strophejs/src/polyfills", "strophe.disco": "components/strophejs-plugins/disco/strophe.disco", - "strophe.vcard": "src/strophe.vcard", "strophe.ping": "src/strophe.ping", + "strophe.rsm": "components/strophejs-plugins/rsm/strophe.rsm", + "strophe.vcard": "src/strophe.vcard", "text": 'components/requirejs-text/text', "tpl": 'components/requirejs-tpl-jcbrand/tpl', "typeahead": "components/typeahead.js/index", @@ -185,10 +186,9 @@ require.config({ 'crypto.sha1': { deps: ['crypto.core'] }, 'crypto.sha256': { deps: ['crypto.core'] }, 'bigint': { deps: ['crypto'] }, - 'strophe.disco': { deps: ['strophe'] }, + 'strophe.ping': { deps: ['strophe'] }, 'strophe.register': { deps: ['strophe'] }, - 'strophe.vcard': { deps: ['strophe'] }, - 'strophe.ping': { deps: ['strophe'] } + 'strophe.vcard': { deps: ['strophe'] } } }); diff --git a/spec/mam.js b/spec/mam.js index 11928f7b1..697a74ed6 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -161,7 +161,7 @@ Date.prototype.getTimezoneOffset = function () { return -120; }; - converse_api.archive.query({'start': '2010-06-07T00:00:00Z', 'limit':10}); + converse_api.archive.query({'start': '2010-06-07T00:00:00Z', 'max':10}); var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); expect(sent_stanza.toString()).toBe( ""+ @@ -202,7 +202,7 @@ converse_api.archive.query({ 'start': '2010-06-07T00:00:00Z', 'after': '09af3-cc343-b409f', - 'limit':10 + 'max':10 }); var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); expect(sent_stanza.toString()).toBe( diff --git a/src/deps-full.js b/src/deps-full.js index 95f60af01..df4c4a237 100644 --- a/src/deps-full.js +++ b/src/deps-full.js @@ -4,9 +4,10 @@ define("converse-dependencies", [ "otr", "moment_with_locales", "strophe", - "strophe.vcard", "strophe.disco", "strophe.ping", + "strophe.rsm", + "strophe.vcard", "backbone.browserStorage", "backbone.overview", "jquery.browser", diff --git a/src/deps-no-otr.js b/src/deps-no-otr.js index 325bfbde5..acab8b5d8 100644 --- a/src/deps-no-otr.js +++ b/src/deps-no-otr.js @@ -3,9 +3,10 @@ define("converse-dependencies", [ "utils", "moment_with_locales", "strophe", - "strophe.vcard", "strophe.disco", "strophe.ping", + "strophe.rsm", + "strophe.vcard", "backbone.browserStorage", "backbone.overview", "jquery.browser", diff --git a/src/deps-website-no-otr.js b/src/deps-website-no-otr.js index fbe3450c3..c9002c8d7 100644 --- a/src/deps-website-no-otr.js +++ b/src/deps-website-no-otr.js @@ -3,9 +3,10 @@ define("converse-dependencies", [ "utils", "moment_with_locales", "strophe", - "strophe.vcard", "strophe.disco", "strophe.ping", + "strophe.rsm", + "strophe.vcard", "bootstrapJS", // XXX: Can be removed, only for https://conversejs.org "backbone.browserStorage", "backbone.overview", diff --git a/src/deps-website.js b/src/deps-website.js index 20cf17907..958429cb8 100644 --- a/src/deps-website.js +++ b/src/deps-website.js @@ -5,9 +5,10 @@ define("converse-dependencies", [ "otr", "moment_with_locales", "strophe", - "strophe.vcard", "strophe.disco", "strophe.ping", + "strophe.rsm", + "strophe.vcard", "bootstrapJS", // XXX: Only for https://conversejs.org "backbone.browserStorage", "backbone.overview", From 7437cf2b42725b3e716e52b32129dc104e0fec0c Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 11 Jul 2015 12:18:05 +0200 Subject: [PATCH 06/45] Bugfix. Closured converse is not defined in utils. --- converse.js | 8 +++++++- src/utils.js | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/converse.js b/converse.js index bcff4a19c..66e4bc49f 100644 --- a/converse.js +++ b/converse.js @@ -1297,7 +1297,12 @@ 'message': '', 'extra_classes': extra_classes }); - $content.append($(message).children('.chat-message-content').first().text(text).addHyperlinks().addEmoticons().parent()); + $content.append( + $(message).children('.chat-message-content').first().text(text) + .addHyperlinks() + .addEmoticons(converse.visible_toolbar_buttons.emoticons) + .parent() + ); this.scrollDown(); }, @@ -6148,6 +6153,7 @@ } } converse.connection.sendIQ(stanza, function (iq) { + debugger; var rsm = new Strophe.RSM({xml: iq.getElementsByTagName('set')[0]}); return _.bind(callback, this, arguments)(rsm); }, errback); diff --git a/src/utils.js b/src/utils.js index fb0d90194..3b3a0855c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -50,8 +50,8 @@ return this; }; - $.fn.addEmoticons = function () { - if (converse.visible_toolbar_buttons.emoticons) { + $.fn.addEmoticons = function (allowed) { + if (allowed) { if (this.length > 0) { this.each(function (i, obj) { var text = $(obj).html(); From a0d344c7c4ea5f7102510fbf3f1d6d2ff95b7324 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 11 Jul 2015 13:40:02 +0200 Subject: [PATCH 07/45] Add better support for the results from a MAM query. updates #306 --- converse.js | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/converse.js b/converse.js index 66e4bc49f..630f1ebd0 100644 --- a/converse.js +++ b/converse.js @@ -542,9 +542,43 @@ converse.connection.send(pres); }; + this.onMAMQueryResult = function (iq, queryid, callback) { + /* Handle the IQ stanza and potential message stanzas returned as + * a result of a MAM (XEP-0313) query. + * + * Parameters: + * (XMLElement) iq - The IQ stanza returned from the XMPP server. + * (String) queryid - A unique ID sent with the MAM query. + * (Function) callback - A function to call whenever we receive query-relevant stanza. + */ + converse.connection.addHandler( + function (message) { + var rsm, $msg = $(message); + var $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); + if ($fin.length) { + rsm = new Strophe.RSM({xml: $fin.find('set')[0]}); + callback(message, rsm); + return false; // We've received all messages, decommission this handler + } + if (typeof callback == "function") { + if (queryid == $msg.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('queryid')) { + callback(message); + } + return true; + } else { + return false; // There's no callback, so no use in continuing this handler. + } + }, null, 'message'); + + if (typeof callback == "function") { + return callback.apply(arguments); + } + }; + this.getVCard = function (jid, callback, errback) { /* Request the VCard of another user. - * Parameters: + * + * Parameters: * (String) jid - The Jabber ID of the user whose VCard is being requested. * (Function) callback - A function to call once the VCard is returned * (Function) errback - A function to call if an error occured @@ -6128,7 +6162,8 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { throw new Error('This server does not support XEP-0313, Message Archive Management'); } - var stanza = $iq({'type':'set'}).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':converse.connection.getUniqueId()}); + var queryid = converse.connection.getUniqueId(); + var stanza = $iq({'type':'set'}).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid}); if (typeof options != "undefined") { stanza.c('x', {'xmlns':'jabber:x:data'}) .c('field', {'var':'FORM_TYPE'}) @@ -6152,11 +6187,7 @@ stanza.cnode(new Strophe.RSM(options).toXML()); } } - converse.connection.sendIQ(stanza, function (iq) { - debugger; - var rsm = new Strophe.RSM({xml: iq.getElementsByTagName('set')[0]}); - return _.bind(callback, this, arguments)(rsm); - }, errback); + converse.connection.sendIQ(stanza, _.partial(converse.onMAMQueryResult, _, queryid, callback), errback); } }, 'rooms': { From 9abbd1e8f0fc1197317edd6100086693cff3563d Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 11 Jul 2015 14:55:02 +0200 Subject: [PATCH 08/45] Allow a Strophe.RSM object to be passed in to archive.query. updates #306 --- converse.js | 40 +++++++++++++++++++++++++++++++++------- spec/mam.js | 2 +- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/converse.js b/converse.js index 92a875da5..3d018238f 100644 --- a/converse.js +++ b/converse.js @@ -122,6 +122,8 @@ // XEP-0059 Result Set Management var RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count']; + // XEP-0313 Message Archive Management + var MAM_ATTRIBUTES = ['with', 'start', 'end']; var STATUS_WEIGHTS = { 'offline': 6, @@ -542,12 +544,13 @@ converse.connection.send(pres); }; - this.onMAMQueryResult = function (iq, queryid, callback) { + this.onMAMQueryResult = function (iq, options, queryid, callback) { /* Handle the IQ stanza and potential message stanzas returned as * a result of a MAM (XEP-0313) query. * * Parameters: * (XMLElement) iq - The IQ stanza returned from the XMPP server. + * (Object) options - The MAM-specific options of the query ('with', 'start' and 'end') * (String) queryid - A unique ID sent with the MAM query. * (Function) callback - A function to call whenever we receive query-relevant stanza. */ @@ -556,7 +559,9 @@ var rsm, $msg = $(message); var $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); if ($fin.length) { - rsm = new Strophe.RSM({xml: $fin.find('set')[0]}); + rsm = new Strophe.RSM( + _.extend({xml: $fin.find('set')[0]}, _.pick(options, MAM_ATTRIBUTES)) + ); callback(message, rsm); return false; // We've received all messages, decommission this handler } @@ -6154,8 +6159,24 @@ }, 'archive': { 'query': function (options, callback, errback) { + /* Do a MAM (XEP-0313) query for archived messages. + * + * Parameters: + * (Object) options - Query parameters, either MAM-specific or also for Result Set Management. + * (Function) callback - A function to call whenever we receive query-relevant stanza. + * (Function) errback - A function to call when an error stanza is received. + * + * The options parameter can also be an instance of + * Strophe.RSM to enable easy querying between results pages. + * + * The callback function may be called multiple times, first + * for the initial IQ result and then for each message + * returned. The last time the callback is called, a + * Strophe.RSM object is returned on which "next" or "previous" + * can be called before passing it in again to this method, to + * get the next or previous page in the result set. + */ var date; - // Available options are jid, limit, start, end, after, before if (typeof options == "function") { callback = options; errback = callback; @@ -6170,8 +6191,8 @@ .c('field', {'var':'FORM_TYPE'}) .c('value').t(Strophe.NS.MAM).up().up(); - if (options.jid) { - stanza.c('field', {'var':'with'}).c('value').t(options.jid).up().up(); + if (options['with']) { + stanza.c('field', {'var':'with'}).c('value').t(options['with']).up().up(); } _.each(['start', 'end'], function (t) { if (options[t]) { @@ -6184,11 +6205,16 @@ } }); stanza.up(); - if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) { + + if (Strophe.RSM.isPrototypeOf(options)) { + stanza.cnode(options.toXML()); + } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) { stanza.cnode(new Strophe.RSM(options).toXML()); } } - converse.connection.sendIQ(stanza, _.partial(converse.onMAMQueryResult, _, queryid, callback), errback); + converse.connection.sendIQ(stanza, + _.partial(converse.onMAMQueryResult, _, options, queryid, callback), + errback); } }, 'rooms': { diff --git a/spec/mam.js b/spec/mam.js index 697a74ed6..e0a16c008 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -45,7 +45,7 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { converse.features.create({'var': Strophe.NS.MAM}); } - converse_api.archive.query({'jid':'juliet@capulet.lit'}); + converse_api.archive.query({'with':'juliet@capulet.lit'}); var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); expect(sent_stanza.toString()).toBe( ""+ From dca0472e808673dfc86a28518e670a292824302a Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 12 Jul 2015 16:29:04 +0200 Subject: [PATCH 09/45] For archive.query, call callback once after receiving all messages. updates #306 --- converse.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/converse.js b/converse.js index 3d018238f..c71bf90d6 100644 --- a/converse.js +++ b/converse.js @@ -552,32 +552,29 @@ * (XMLElement) iq - The IQ stanza returned from the XMPP server. * (Object) options - The MAM-specific options of the query ('with', 'start' and 'end') * (String) queryid - A unique ID sent with the MAM query. - * (Function) callback - A function to call whenever we receive query-relevant stanza. + * (Function) callback - A function to call after we've received all the archived messages. + * If should expect an array of messages and a Strophe.RSM (result set management) object. */ + var messages = []; converse.connection.addHandler( function (message) { - var rsm, $msg = $(message); - var $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); - if ($fin.length) { - rsm = new Strophe.RSM( - _.extend({xml: $fin.find('set')[0]}, _.pick(options, MAM_ATTRIBUTES)) - ); - callback(message, rsm); - return false; // We've received all messages, decommission this handler - } + var $msg = $(message), $fin; if (typeof callback == "function") { - if (queryid == $msg.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('queryid')) { - callback(message); + $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); + if ($fin.length) { + callback( + messages, + new Strophe.RSM(_.extend({xml: $fin.find('set')[0]}, _.pick(options, MAM_ATTRIBUTES))) + ); + return false; // We've received all messages, decommission this handler + } else if (queryid == $msg.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('queryid')) { + messages.push(message); } return true; } else { return false; // There's no callback, so no use in continuing this handler. } }, null, 'message'); - - if (typeof callback == "function") { - return callback.apply(arguments); - } }; this.getVCard = function (jid, callback, errback) { From 69de033cead00af98d0a28d1c0ade9da95e7801c Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 12 Jul 2015 21:27:22 +0200 Subject: [PATCH 10/45] Write documentation for the archive API. Also add another test. update #306 --- docs/source/development.rst | 155 +++++++++++++++++++++++++++++++++--- spec/mam.js | 29 +++++++ 2 files changed, 173 insertions(+), 11 deletions(-) diff --git a/docs/source/development.rst b/docs/source/development.rst index a056ef1e2..b22d3f570 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -51,14 +51,14 @@ directory: On Windows you need to specify Makefile.win to be used by running: :: make -f Makefile.win dev - + Or alternatively, if you don't have GNU Make: :: npm install bower update - + This will first install the Node.js development tools (like Grunt and Bower) and then use Bower to install all of Converse.js's front-end dependencies. @@ -125,7 +125,7 @@ Please read the `style guide `_ and make sure that Add tests for your bugfix or feature ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a test for any bug fixed or feature added. We use Jasmine -for testing. +for testing. Take a look at `tests.html `_ and the `spec files `_ @@ -146,7 +146,7 @@ Developer API Earlier versions of Converse.js might have different API methods or none at all. In the Converse.js API, you traverse towards a logical grouping, from -which you can then call certain standardised accessors and mutators, like:: +which you can then call certain standardised accessors and mutators, such as:: .get .set @@ -202,6 +202,139 @@ Example: roster_groups: true }); +The "archive" grouping +---------------------- + +Converse.js supports the *Message Archive Management* +(`XEP-0313 `_) protocol, +through which it is able to query an XMPP server for archived messages. + +query +~~~~~ + +The ``query`` method is used to query for archived messages. + +It accepts the following optional parameters: + +* **options** an object containing the query parameters. Valid query parameters + are ``with``, ``start``, ``end``, ``first``, ``last``, ``after``, ``before``, ``index`` and ``count``. +* **callback** is the callback method that will be called when all the messages + have been received. +* **errback** is the callback method to be called when an error is returned by + the XMPP server, for example when it doesn't support message archiving. + +Examples +^^^^^^^^ + +**Requesting all archived messages** + +The simplest query that can be made is to simply not pass in any parameters. +Such a query will return all archived messages for the current user. + +Generally, you'll however always want to pass in a callback method, to receive +the returned messages. + +.. code-block:: javascript + + var errback = function (iq) { + // The query was not successful, perhaps inform the user? + // The IQ stanza returned by the XMPP server is passed in, so that you + // may inspect it and determine what the problem was. + } + var callback = function (messages) { + // Do something with the messages, like showing them in your webpage. + } + converse.archive.query(callback, errback)) + + +**Requesting all archived messages for a particular contact or room** + +To query for messages sent between the current user and another user or room, +the query options need to contain the the JID (Jabber ID) of the user or +room under the ``with`` key. + +.. code-block:: javascript + + // For a particular user + converse.archive.query({'with': 'john@doe.net'}, callback, errback);) + + // For a particular room + converse.archive.query({'with': 'discuss@conference.doglovers.net'}, callback, errback);) + + +**Requesting all archived messages before or after a certain date** + +The ``start`` and ``end`` parameters are used to query for messages +within a certain timeframe. The passed in date values may either be ISO8601 +formatted date strings, or Javascript Date objects. + +.. code-block:: javascript + + var options = { + 'with': 'john@doe.net', + 'start': '2010-06-07T00:00:00Z', + 'end': '2010-07-07T13:23:54Z' + }; + converse.archive.query(options, callback, errback); + + +**Limiting the amount of messages returned** + +The amount of returned messages may be limited with the ``max`` parameter. +By default, the messages are returned from oldest to newest. + +.. code-block:: javascript + + // Return maximum 10 archived messages + converse.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); + + +**Paging forwards through a set of archived messages** + +When limiting the amount of messages returned per query, you might want to +repeatedly make a further query to fetch the next batch of messages. + +To simplify this usecase for you, the callback method receives not only an array +with the returned archived messages, but also a special RSM (*Result Set +Management*) object which contains the query parameters you passed in, as well +as two utility methods ``next``, and ``previous``. + +When you call one of these utility methods on the returned RSM object, and then +pass the result into a new query, you'll receive the next or previous batch of +archived messages. + +.. code-block:: javascript + + var callback = function (messages, rsm) { + // Do something with the messages, like showing them in your webpage. + // ... + // You can now use the returned "rsm" object, to fetch the next batch of messages: + converse.archive.query(rsm.next(), callback, errback)) + + } + converse.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); + +**Paging backwards through a set of archived messages** + +To page backwards through the archive, you need to know the UID of the message +which you'd like to page backwards from and then pass that as value for the +``before`` parameter. If you simply want to page backwards from the most recent +message, pass in the ``before`` parameter with a value of ``null``. + +.. code-block:: javascript + + converse.archive.query({'before': null, 'max':5}, function (message, rsm) { + // Do something with the messages, like showing them in your webpage. + // ... + // You can now use the returned "rsm" object, to fetch the previous batch of messages: + rsm.previous(); // Call previous method, to update the object + // parameters so that the previous batch of messages will be returned. + rsm.count = 10; // Increase the page size (currently it's set to 1, + // because that was what we specified in our last query. + // Now we query again, to get the previous batch. + converse.archive.query(rsm, callback, errback); + } + The "user" grouping ------------------- @@ -213,7 +346,7 @@ logout Log the user out of the current XMPP session. -.. code-block:: javascript +.. code-block:: javascript converse.user.logout(); @@ -228,7 +361,7 @@ get Return the current user's availability status: -.. code-block:: javascript +.. code-block:: javascript converse.user.status.get(); // Returns for example "dnd" @@ -246,7 +379,7 @@ The user's status can be set to one of the following values: For example: -.. code-block:: javascript +.. code-block:: javascript converse.user.status.set('dnd'); @@ -254,7 +387,7 @@ Because the user's availability is often set together with a custom status message, this method also allows you to pass in a status message as a second parameter: -.. code-block:: javascript +.. code-block:: javascript converse.user.status.set('dnd', 'In a meeting'); @@ -264,7 +397,7 @@ The "message" sub-grouping The ``user.status.message`` sub-grouping exposes methods for setting and retrieving the user's custom status message. -.. code-block:: javascript +.. code-block:: javascript converse.user.status.message.set('In a meeting'); @@ -344,7 +477,7 @@ Provide the JID of the contact you want to add: .. code-block:: javascript converse.contacts.add('buddy@example.com') - + You may also provide the fullname. If not present, we use the jid as fullname: .. code-block:: javascript @@ -663,7 +796,7 @@ An example plugin }(this, function ($, strophe, utils, converse_api) { // Wrap your UI strings with the __ function for translation support. - var __ = $.proxy(utils.__, this); + var __ = $.proxy(utils.__, this); // Strophe methods for building stanzas var Strophe = strophe.Strophe; diff --git a/spec/mam.js b/spec/mam.js index e0a16c008..6ad844d4c 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -227,6 +227,35 @@ Date.prototype.getTimezoneOffset = getTimezoneOffset; }); + it("accepts \"before\" with an empty string as value to reverse the order", function () { + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + converse_api.archive.query({'before': '', 'max':10}); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + ""+ + ""+ + "urn:xmpp:mam:0"+ + ""+ + ""+ + ""+ + "10"+ + ""+ + ""+ + ""+ + "" + ); + }); + }, converse, mock, test_utils)); describe("The default preference", $.proxy(function (mock, test_utils) { From 27afd32aebe72b4b25eeade83003f7a2fe1b62e7 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 12 Jul 2015 22:00:32 +0200 Subject: [PATCH 11/45] Test that archive.query takes a Strophe.RSM object. updates #306 --- spec/mam.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spec/mam.js b/spec/mam.js index 6ad844d4c..7e1041399 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -256,6 +256,55 @@ ); }); + it("accepts a Strophe.RSM object for the query options", function () { + // Normally the user wouldn't manually make a Strophe.RSM object + // and pass it in. However, in the callback method an RSM object is + // returned which can be reused for easy paging. This test is + // more for that usecase. + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + // Mock the browser's method for returning the timezone + var getTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function () { + return -120; + }; + var rsm = new Strophe.RSM({'max': '10'}); + rsm['with'] = 'romeo@montague.lit'; + rsm.start = '2010-06-07T00:00:00Z'; + converse_api.archive.query(rsm); + + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + expect(sent_stanza.toString()).toBe( + ""+ + ""+ + ""+ + ""+ + "urn:xmpp:mam:0"+ + ""+ + ""+ + "romeo@montague.lit"+ + ""+ + ""+ + "2010-06-07T02:00:00+02:00"+ + ""+ + ""+ + ""+ + "10"+ + ""+ + ""+ + "" + ); + // Restore + Date.prototype.getTimezoneOffset = getTimezoneOffset; + }); + }, converse, mock, test_utils)); describe("The default preference", $.proxy(function (mock, test_utils) { From 1a9771c7172737f86bb7d168bdbdde51213130a9 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 12 Jul 2015 23:58:07 +0200 Subject: [PATCH 12/45] Test code which receives archived messages and calls callback updates #306 --- converse.js | 14 +++++++------- spec/mam.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/converse.js b/converse.js index c71bf90d6..77d9e760d 100644 --- a/converse.js +++ b/converse.js @@ -558,23 +558,23 @@ var messages = []; converse.connection.addHandler( function (message) { - var $msg = $(message), $fin; + var $msg = $(message), $fin, rsm, i; if (typeof callback == "function") { $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); if ($fin.length) { - callback( - messages, - new Strophe.RSM(_.extend({xml: $fin.find('set')[0]}, _.pick(options, MAM_ATTRIBUTES))) - ); + rsm = new Strophe.RSM({xml: $fin.find('set')[0]}); + _.extend(rsm, _.pick(options, ['max', 'after', 'before'])); + _.extend(rsm, _.pick(options, MAM_ATTRIBUTES)); + callback(messages, rsm); return false; // We've received all messages, decommission this handler - } else if (queryid == $msg.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('queryid')) { + } else if (queryid == $msg.find('result').attr('queryid')) { messages.push(message); } return true; } else { return false; // There's no callback, so no use in continuing this handler. } - }, null, 'message'); + }, Strophe.NS.MAM, 'message'); }; this.getVCard = function (jid, callback, errback) { diff --git a/spec/mam.js b/spec/mam.js index 7e1041399..846a6f2ba 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -12,6 +12,7 @@ var Strophe = converse_api.env.Strophe; var $iq = converse_api.env.$iq; var $pres = converse_api.env.$pres; + var $msg = converse_api.env.$msg; // See: https://xmpp.org/rfcs/rfc3921.html describe("Message Archive Management", $.proxy(function (mock, test_utils) { @@ -261,15 +262,15 @@ // and pass it in. However, in the callback method an RSM object is // returned which can be reused for easy paging. This test is // more for that usecase. + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } var sent_stanza, IQ_id; var sendIQ = converse.connection.sendIQ; spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); - if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { - converse.features.create({'var': Strophe.NS.MAM}); - } // Mock the browser's method for returning the timezone var getTimezoneOffset = Date.prototype.getTimezoneOffset; Date.prototype.getTimezoneOffset = function () { @@ -305,6 +306,55 @@ Date.prototype.getTimezoneOffset = getTimezoneOffset; }); + it("accepts a callback function, which it passes the messages and a Strophe.RSM object", function () { + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.features.create({'var': Strophe.NS.MAM}); + } + var sent_stanza, IQ_id; + var sendIQ = converse.connection.sendIQ; + spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + spyOn(converse, 'onMAMQueryResult').andCallThrough(); + var callback = jasmine.createSpy('callback'); + + converse_api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback); + var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); + + // Send the result stanza, so that the callback is called. + var stanza = $iq({'type': 'result', 'id': IQ_id}); + converse.connection._dataRecv(test_utils.createRequest(stanza)); + expect(converse.onMAMQueryResult).toHaveBeenCalled(); + + /* Send a message to indicate the end of the result set. + * + * + * + * + * 23452-4534-1 + * 390-2342-22 + * 16 + * + * + * + */ + stanza = $msg().c('fin', {'xmlns': 'urn:xmpp:mam:0', 'complete': 'true'}) + .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) + .c('first', {'index': '0'}).t('23452-4534-1').up() + .c('last').t('390-2342-22').up() + .c('count').t('16'); + converse.connection._dataRecv(test_utils.createRequest(stanza)); + + expect(callback).toHaveBeenCalled(); + var args = callback.argsForCall[0]; + expect(args[1]['with']).toBe('romeo@capulet.lit'); + expect(args[1].max).toBe('10'); + expect(args[1].count).toBe('16'); + expect(args[1].first).toBe('23452-4534-1'); + expect(args[1].last).toBe('390-2342-22'); + }); + }, converse, mock, test_utils)); describe("The default preference", $.proxy(function (mock, test_utils) { From 087718d7f0d026914478e109a500b828b91bae54 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 13 Jul 2015 23:16:21 +0200 Subject: [PATCH 13/45] Expand a test case to also check for received archived messages. updates #306 --- spec/mam.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/spec/mam.js b/spec/mam.js index 846a6f2ba..311dfde27 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -327,6 +327,45 @@ converse.connection._dataRecv(test_utils.createRequest(stanza)); expect(converse.onMAMQueryResult).toHaveBeenCalled(); + /* + * + * + * + * + * Call me but love, and I'll be new baptized; Henceforth I never will be Romeo. + * + * + * + * + */ + var msg1 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'}) + .c('result', {'xmlns': 'urn:xmpp:mam:0', 'queryid':queryid, 'id':'28482-98726-73623'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to':'juliet@capulet.lit/balcony', + 'from':'romeo@montague.lit/orchard', + 'type':'chat' }) + .c('body').t("Call me but love, and I'll be new baptized;"); + converse.connection._dataRecv(test_utils.createRequest(msg1)); + + var msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'}) + .c('result', {'xmlns': 'urn:xmpp:mam:0', 'queryid':queryid, 'id':'28482-98726-73624'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to':'juliet@capulet.lit/balcony', + 'from':'romeo@montague.lit/orchard', + 'type':'chat' }) + .c('body').t("Henceforth I never will be Romeo."); + converse.connection._dataRecv(test_utils.createRequest(msg2)); + /* Send a message to indicate the end of the result set. * * @@ -348,6 +387,9 @@ expect(callback).toHaveBeenCalled(); var args = callback.argsForCall[0]; + expect(args[0].length).toBe(2); + expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML); + expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML); expect(args[1]['with']).toBe('romeo@capulet.lit'); expect(args[1].max).toBe('10'); expect(args[1].count).toBe('16'); From 393dd6150d0950a561ec62c97be1afd8b6e649c3 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 16:42:00 +0200 Subject: [PATCH 14/45] Use the Strophe.NS.XFORM constant --- converse.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/converse.js b/converse.js index 1447f5005..b2f5d9db3 100644 --- a/converse.js +++ b/converse.js @@ -1256,10 +1256,6 @@ } }, - insertIntoPage: function () { - this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); - }, - render: function () { this.$el.attr('id', this.model.get('box_id')) .html(converse.templates.chatbox( @@ -1276,6 +1272,11 @@ return this.showStatusMessage(); }, + insertIntoPage: function () { + this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); + return this; + }, + initDragResize: function () { this.prev_pageY = 0; // To store last known mouse position if (converse.connection.connected) { @@ -2927,7 +2928,7 @@ // Send an IQ stanza with the room configuration. var iq = $iq({to: this.model.get('jid'), type: "set"}) .c("query", {xmlns: Strophe.NS.MUC_OWNER}) - .c("x", {xmlns: "jabber:x:data", type: "submit"}); + .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"}); _.each(config, function (node) { iq.cnode(node).up(); }); return converse.connection.sendIQ(iq.tree(), onSuccess, onError); }, @@ -6185,7 +6186,7 @@ var queryid = converse.connection.getUniqueId(); var stanza = $iq({'type':'set'}).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid}); if (typeof options != "undefined") { - stanza.c('x', {'xmlns':'jabber:x:data'}) + stanza.c('x', {'xmlns':Strophe.NS.XFORM}) .c('field', {'var':'FORM_TYPE'}) .c('value').t(Strophe.NS.MAM).up().up(); From d992efc34bed9e6aa38f3f562eece865512d4dca Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 16:42:58 +0200 Subject: [PATCH 15/45] Optimization: only query for MAM prefs when necessary updates #306 --- converse.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/converse.js b/converse.js index b2f5d9db3..c85bc7666 100644 --- a/converse.js +++ b/converse.js @@ -5103,7 +5103,8 @@ }, onFeatureAdded: function (feature) { - if (feature.get('var') == Strophe.NS.MAM) { + var prefs = feature.get('preferences') || {}; + if (feature.get('var') == Strophe.NS.MAM && prefs['default'] !== converse.message_archiving) { // Ask the server for archiving preferences converse.connection.sendIQ( $iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}), From 8625d1daba7f2ea104e839735a04a335f5c9034d Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 16:49:58 +0200 Subject: [PATCH 16/45] Add initial support for fetching and showing archived messages. In the proces I added converse.chatboxes.getChatBox which allowed me to remove the getWrappedChatBox method. --- converse.js | 201 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 124 insertions(+), 77 deletions(-) diff --git a/converse.js b/converse.js index c85bc7666..2b49f997d 100644 --- a/converse.js +++ b/converse.js @@ -301,6 +301,7 @@ allow_logout: true, allow_muc: true, allow_otr: true, + archived_messages_batch_size: '20', auto_away: 0, // Seconds after which user status is set to 'away' auto_xa: 0, // Seconds after which user status is set to 'xa' allow_registration: true, @@ -1140,9 +1141,10 @@ this.save({'otr_status': UNENCRYPTED}); }, - createMessage: function ($message) { + createMessage: function ($message, $delay) { + $delay = $delay || $message.find('delay'); var body = $message.children('body').text(), - delayed = $message.find('delay').length > 0, + delayed = $delay.length > 0, fullname = this.get('fullname'), is_groupchat = $message.attr('type') === 'groupchat', msgid = $message.attr('id'), @@ -1160,7 +1162,7 @@ } fullname = (_.isEmpty(fullname) ? from: fullname).split(' ')[0]; if (delayed) { - stamp = $message.find('delay').attr('stamp'); + stamp = $delay.attr('stamp'); time = stamp; } else { time = moment().format(); @@ -1181,11 +1183,11 @@ }); }, - receiveMessage: function ($message) { + receiveMessage: function ($message, $delay) { var $body = $message.children('body'); var text = ($body.length > 0 ? $body.text() : undefined); if ((!text) || (!converse.allow_otr)) { - return this.createMessage($message); + return this.createMessage($message, $delay); } if (text.match(/^\?OTRv23?/)) { this.initiateOTR(text); @@ -1201,7 +1203,7 @@ } } else { // Normal unencrypted message. - this.createMessage($message); + this.createMessage($message, $delay); } } } @@ -1234,6 +1236,7 @@ this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); // TODO check for changed fullname as well + this.model.on('change:archived_count', this.fetchArchivedMessages, this); this.model.on('change:chat_state', this.sendChatState, this); this.model.on('change:chat_status', this.onChatStatusChanged, this); this.model.on('change:image', this.renderAvatar, this); @@ -1249,8 +1252,8 @@ this.model.on('showReceivedOTRMessage', function (text) { this.showMessage({'message': text, 'sender': 'them'}); }, this); - this.updateVCard().insertIntoPage(); - this.hide().render().model.messages.fetch({add: true}); + this.updateVCard().insertIntoPage().fetchMessages(); + if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) { this.model.initiateOTR(); } @@ -1272,6 +1275,71 @@ return this.showStatusMessage(); }, + fetchMessages: function () { + /* Responsible for fetching previously sent messages, first + * from session storage, and then once that's done by calling + * fetchArchivedMessages, which fetches from the XMPP server if + * applicable. + */ + this.hide().render().model.messages.fetch({ + 'add': true, + 'success': this.afterFetchingCachedMessages.bind(this) + }); + }, + + afterFetchingCachedMessages: function () { + /* Handler method, called after messages cached in + * sessionStorage have been fetched. + * + * The goal of this method is to determine how many archived + * messages exist and whether we should fetch them or not. + */ + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + return; + } + if (typeof this.model.get('archived_count') == 'undefined') { + // Get the amount of archived messages + // Refer to: https://xmpp.org/extensions/xep-0059.html#count + API.archive.query({ + 'with': this.model.get('jid'), + 'max': 0 + }, + function (messages, attrs) { // On Success + // Whenever the archived_count attribute changes, + // fetchArchivedMessages will be called. + this.model.save({'archived_count': Number(attrs.count)}); + }.bind(this), + function (iq) { // On Error + converse.log("Error occured while trying to fetch the archived messages count", "error"); + this.model.save({'archived_count': 0}); + }.bind(this) + ); + } else { + this.fetchArchivedMessages(); + } + }, + + fetchArchivedMessages: function () { + /* Fetch archived chat messages if we know that there are more + * than zero of them. + * + * Then, upon receiving them, call onMessage on the chat box, + * so that they are displayed inside it. + */ + if (this.model.messages.length < this.model.get('archived_count') && + this.model.messages.length < converse.archived_messages_batch_size) { + + // TODO: fetch only messages we don't yet have + API.archive.query({ + 'with': this.model.get('jid'), + 'max': converse.archived_messages_batch_size + }, + _.partial(_.map, _, converse.chatboxes.onMessage.bind(converse.chatboxes)), + _.partial(converse.log, "Error while trying to fetch archived messages", "error") + ); + } + }, + insertIntoPage: function () { this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); return this; @@ -1358,6 +1426,9 @@ }, onMessageAdded: function (message) { + // TODO: properly insert messages in the right place, indicate + // messages from different days (current code doesn't go far + // enough). var time = message.get('time'), times = this.model.messages.pluck('time'), previous_message, idx, this_date, prev_date, text, match; @@ -3334,7 +3405,7 @@ /* Handler method for all incoming single-user chat "message" stanzas. */ var $message = $(message), - contact_jid, $forwarded, $received, $sent, from_bare_jid, from_resource, is_me, + contact_jid, $forwarded, $delay, from_bare_jid, from_resource, is_me, msgid = $message.attr('id'), chatbox, resource, roster_item, from_jid = $message.attr('from'), @@ -3350,17 +3421,10 @@ converse.log("Ignore incoming message sent from this client's JID: "+from_jid, 'info'); return true; } - $forwarded = $message.children('forwarded'); - $received = $message.children('received[xmlns="urn:xmpp:carbons:2"]'); - $sent = $message.children('sent[xmlns="urn:xmpp:carbons:2"]'); - + $forwarded = $message.find('forwarded'); if ($forwarded.length) { $message = $forwarded.children('message'); - } else if ($received.length) { - $message = $received.children('forwarded').children('message'); - from_jid = $message.attr('from'); - } else if ($sent.length) { - $message = $sent.children('forwarded').children('message'); + $delay = $forwarded.children('delay'); from_jid = $message.attr('from'); to_jid = $message.attr('to'); } @@ -3376,46 +3440,47 @@ contact_jid = from_bare_jid; resource = from_resource; } + // Get chat box, but only create a new one when the message has a body. + chatbox = this.getChatBox(contact_jid, $message.find('body').length > 0); - roster_item = converse.roster.get(contact_jid); - if (roster_item === undefined) { - // The contact was likely removed - converse.log('Could not get roster item for JID '+contact_jid, 'error'); - return true; - } - - chatbox = this.get(contact_jid); - if (!chatbox) { - /* If chat state notifications (because a roster contact - * closed a chat box of yours they had open) are received - * and we don't have a chat with the user, then we do not - * want to open a chat box. We only open a new chat box when - * the message has a body. - */ - if ($message.find('body').length === 0) { - return true; - } - var fullname = roster_item.get('fullname'); - fullname = _.isEmpty(fullname)? contact_jid: fullname; - chatbox = this.create({ - 'id': contact_jid, - 'jid': contact_jid, - 'fullname': fullname, - 'image_type': roster_item.get('image_type'), - 'image': roster_item.get('image'), - 'url': roster_item.get('url') - }); - } if (msgid && chatbox.messages.findWhere({msgid: msgid})) { return true; // We already have this message stored. } if (!this.isOnlyChatStateNotification($message) && !is_me) { converse.playNotification(); } - chatbox.receiveMessage($message); + chatbox.receiveMessage($message, $delay); converse.roster.addResource(contact_jid, resource); converse.emit('message', message); return true; + }, + + getChatBox: function (jid, create) { + /* Returns a chat box or optionally return a newly + * created one if one doesn't exist. + * + * Parameters: + * (String) jid - The JID of the user whose chat box we want + * (Boolean) create - Should a new chat box be created if none exists? + */ + var bare_jid = Strophe.getBareJidFromJid(jid); + var chatbox = this.get(bare_jid); + if (!chatbox && create) { + var roster_item = converse.roster.get(bare_jid); + if (roster_item === undefined) { + converse.log('Could not get roster item for JID '+bare_jid, 'error'); + return; + } + chatbox = this.create({ + 'id': bare_jid, + 'jid': bare_jid, + 'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'), + 'image_type': roster_item.get('image_type'), + 'image': roster_item.get('image'), + 'url': roster_item.get('url') + }); + } + return chatbox; } }); @@ -6014,6 +6079,7 @@ }; var wrappedChatBox = function (chatbox) { + if (!chatbox) { return; } var view = converse.chatboxviews.get(chatbox.get('jid')); return { 'close': view.close.bind(view), @@ -6029,28 +6095,7 @@ }; }; - var getWrappedChatBox = function (jid) { - var bare_jid = Strophe.getBareJidFromJid(jid); - var chatbox = converse.chatboxes.get(bare_jid); - if (!chatbox) { - var roster_item = converse.roster.get(bare_jid); - if (roster_item === undefined) { - converse.log('Could not get roster item for JID '+bare_jid, 'error'); - return null; - } - chatbox = converse.chatboxes.create({ - 'id': bare_jid, - 'jid': bare_jid, - 'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'), - 'image_type': roster_item.get('image_type'), - 'image': roster_item.get('image'), - 'url': roster_item.get('url') - }); - } - return wrappedChatBox(chatbox); - }; - - return { + var API = { 'initialize': function (settings, callback) { converse.initialize(settings, callback); }, @@ -6137,12 +6182,12 @@ converse.log("chats.open: You need to provide at least one JID", "error"); return null; } else if (typeof jids === "string") { - chatbox = getWrappedChatBox(jids); + chatbox = wrappedChatBox(converse.chatboxes.getChatBox(jids, true)); chatbox.open(); return chatbox; } return _.map(jids, function (jid) { - var chatbox = getWrappedChatBox(jid); + chatbox = wrappedChatBox(converse.chatboxes.getChatBox(jid, true)); chatbox.open(); return chatbox; }); @@ -6152,9 +6197,9 @@ converse.log("chats.get: You need to provide at least one JID", "error"); return null; } else if (typeof jids === "string") { - return getWrappedChatBox(jids); + return wrappedChatBox(converse.chatboxes.getChatBox(jids, true)); } - return _.map(jids, getWrappedChatBox); + return _.map(jids, _.partial(_.compose(wrappedChatBox, converse.chatboxes.getChatBox.bind(converse.chatboxes)), _, true)); } }, 'archive': { @@ -6238,7 +6283,7 @@ 'box_id' : b64_sha1(jid) }); } - return wrappedChatBox(chatroom); + return wrappedChatBox(converse.chatboxes.getChatBox(chatroom, true)); }; if (typeof jids === "undefined") { throw new TypeError('rooms.open: You need to provide at least one JID'); @@ -6251,9 +6296,10 @@ if (typeof jids === "undefined") { throw new TypeError("rooms.get: You need to provide at least one JID"); } else if (typeof jids === "string") { - return getWrappedChatBox(jids); + return wrappedChatBox(converse.chatboxes.getChatBox(jids, true)); } - return _.map(jids, getWrappedChatBox); + return _.map(jids, _.partial(wrappedChatBox, _.bind(converse.chatboxes.getChatBox, converse.chatboxes, _, true))); + } }, 'tokens': { @@ -6329,4 +6375,5 @@ 'moment': moment } }; + return API; })); From 080d86e7fec0369721caac9335257da9463b90a2 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 17:55:15 +0200 Subject: [PATCH 17/45] Show most recent archived messages first. - Also ensure that the archive.query API always return messages chronologically. - Fix docs which wrongly state that null can be passed in. updates #306 --- converse.js | 2 ++ docs/source/development.rst | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/converse.js b/converse.js index 2b49f997d..de4cdc159 100644 --- a/converse.js +++ b/converse.js @@ -564,6 +564,7 @@ $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); if ($fin.length) { rsm = new Strophe.RSM({xml: $fin.find('set')[0]}); + if (typeof options.before !== "undefined") { messages.reverse(); } _.extend(rsm, _.pick(options, ['max', 'after', 'before'])); _.extend(rsm, _.pick(options, MAM_ATTRIBUTES)); callback(messages, rsm); @@ -1331,6 +1332,7 @@ // TODO: fetch only messages we don't yet have API.archive.query({ + 'before': '', // Page backwards from the most recent message 'with': this.model.get('jid'), 'max': converse.archived_messages_batch_size }, diff --git a/docs/source/development.rst b/docs/source/development.rst index b22d3f570..6cfca806e 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -319,11 +319,11 @@ archived messages. To page backwards through the archive, you need to know the UID of the message which you'd like to page backwards from and then pass that as value for the ``before`` parameter. If you simply want to page backwards from the most recent -message, pass in the ``before`` parameter with a value of ``null``. +message, pass in the ``before`` parameter with an empty string value ``''``. .. code-block:: javascript - converse.archive.query({'before': null, 'max':5}, function (message, rsm) { + converse.archive.query({'before': '', 'max':5}, function (message, rsm) { // Do something with the messages, like showing them in your webpage. // ... // You can now use the returned "rsm" object, to fetch the previous batch of messages: From 10e53468fb3494ba78a98efe1252b0f170d95836 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 20:11:13 +0200 Subject: [PATCH 18/45] Initial work on adding infinite scrolling for archived messages. updates #306 --- converse.js | 75 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/converse.js b/converse.js index de4cdc159..a09a77b42 100644 --- a/converse.js +++ b/converse.js @@ -564,8 +564,7 @@ $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); if ($fin.length) { rsm = new Strophe.RSM({xml: $fin.find('set')[0]}); - if (typeof options.before !== "undefined") { messages.reverse(); } - _.extend(rsm, _.pick(options, ['max', 'after', 'before'])); + _.extend(rsm, _.pick(options, ['max'])); _.extend(rsm, _.pick(options, MAM_ATTRIBUTES)); callback(messages, rsm); return false; // We've received all messages, decommission this handler @@ -1237,7 +1236,7 @@ this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); // TODO check for changed fullname as well - this.model.on('change:archived_count', this.fetchArchivedMessages, this); + this.model.on('change:archived_count', this.maybeFetchArchivedMessages.bind(this)); this.model.on('change:chat_state', this.sendChatState, this); this.model.on('change:chat_status', this.onChatStatusChanged, this); this.model.on('change:image', this.renderAvatar, this); @@ -1253,7 +1252,7 @@ this.model.on('showReceivedOTRMessage', function (text) { this.showMessage({'message': text, 'sender': 'them'}); }, this); - this.updateVCard().insertIntoPage().fetchMessages(); + this.updateVCard().render().fetchMessages().insertIntoPage().hide(); if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) { this.model.initiateOTR(); @@ -1271,21 +1270,33 @@ ) ); this.renderToolbar().renderAvatar(); + this.$el.find('.chat-content').on('scroll', _.debounce(this.onScroll.bind(this), 100)); converse.emit('chatBoxOpened', this); setTimeout(converse.refreshWebkit, 50); return this.showStatusMessage(); }, + onScroll: function (ev) { + var rsm = this.model.get('rsm'); + if (rsm && $(ev.target).scrollTop() === 0) { + if (! (rsm instanceof Strophe.RSM)) { + rsm = new Strophe.RSM(rsm); + } + this.fetchArchivedMessages(rsm.previous()); + } + }, + fetchMessages: function () { /* Responsible for fetching previously sent messages, first * from session storage, and then once that's done by calling * fetchArchivedMessages, which fetches from the XMPP server if * applicable. */ - this.hide().render().model.messages.fetch({ + this.model.messages.fetch({ 'add': true, 'success': this.afterFetchingCachedMessages.bind(this) }); + return this; }, afterFetchingCachedMessages: function () { @@ -1316,30 +1327,35 @@ }.bind(this) ); } else { + this.maybeFetchArchivedMessages(); + } + }, + + maybeFetchArchivedMessages: function () { + if (this.model.messages.length < this.model.get('archived_count') && + this.model.messages.length < converse.archived_messages_batch_size) { this.fetchArchivedMessages(); } }, - fetchArchivedMessages: function () { - /* Fetch archived chat messages if we know that there are more - * than zero of them. + fetchArchivedMessages: function (rsm) { + /* Fetch archived chat messages from the XMPP server. * * Then, upon receiving them, call onMessage on the chat box, * so that they are displayed inside it. */ - if (this.model.messages.length < this.model.get('archived_count') && - this.model.messages.length < converse.archived_messages_batch_size) { - - // TODO: fetch only messages we don't yet have - API.archive.query({ - 'before': '', // Page backwards from the most recent message - 'with': this.model.get('jid'), - 'max': converse.archived_messages_batch_size - }, - _.partial(_.map, _, converse.chatboxes.onMessage.bind(converse.chatboxes)), - _.partial(converse.log, "Error while trying to fetch archived messages", "error") - ); - } + API.archive.query( + rsm instanceof Strophe.RSM ? rsm : { + 'before': '', // Page backwards from the most recent message + 'with': this.model.get('jid'), + 'max': converse.archived_messages_batch_size + }, + function (messages, rsm) { + this.model.save({'rsm': rsm}); + _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes)); + }.bind(this), + _.partial(converse.log, "Error while trying to fetch archived messages", "error") + ); }, insertIntoPage: function () { @@ -1380,7 +1396,13 @@ match = text.match(/^\/(.*?)(?: (.*))?$/), fullname = this.model.get('fullname') || msg_dict.fullname, extra_classes = msg_dict.delayed && 'delayed' || '', - template, username; + template, username, insertMessage; + + if (this.model.messages.length && moment(msg_time).isBefore(this.model.messages.at(0).get('time'))) { + insertMessage = $content.prepend.bind($content); + } else { + insertMessage = _.compose(this.scrollDown.bind(this), $content.append.bind($content)); + } if ((match) && (match[1] === 'me')) { text = text.replace(/^\/me/, ''); @@ -1404,13 +1426,12 @@ 'message': '', 'extra_classes': extra_classes }); - $content.append( + insertMessage( $(message).children('.chat-message-content').first().text(text) .addHyperlinks() .addEmoticons(converse.visible_toolbar_buttons.emoticons) .parent() ); - this.scrollDown(); }, showHelpMessages: function (msgs, type, spinner) { @@ -1428,9 +1449,6 @@ }, onMessageAdded: function (message) { - // TODO: properly insert messages in the right place, indicate - // messages from different days (current code doesn't go far - // enough). var time = message.get('time'), times = this.model.messages.pluck('time'), previous_message, idx, this_date, prev_date, text, match; @@ -1469,7 +1487,6 @@ if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) { converse.incrementMsgCounter(); } - this.scrollDown(); if (!this.model.get('minimized') && !this.$el.is(':visible')) { this.show(); } @@ -6253,7 +6270,7 @@ }); stanza.up(); - if (Strophe.RSM.isPrototypeOf(options)) { + if (options instanceof Strophe.RSM) { stanza.cnode(options.toXML()); } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) { stanza.cnode(new Strophe.RSM(options).toXML()); From 99c7b9cb95aa16932acb5892eef259cfc0f06c94 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 20:33:31 +0200 Subject: [PATCH 19/45] Add a new emitted event: serviceDiscovered. Gets thrown every time converse learns of a supported server service. Useful when 3rd party code wants to use API methods that depend on the service being discovered first. --- converse.js | 1 + docs/CHANGES.rst | 1 + docs/source/development.rst | 36 +++++++++++++++++++----------------- spec/converse.js | 4 ++++ tests/main.js | 1 + 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/converse.js b/converse.js index a09a77b42..da2a9965d 100644 --- a/converse.js +++ b/converse.js @@ -5188,6 +5188,7 @@ onFeatureAdded: function (feature) { var prefs = feature.get('preferences') || {}; + converse.emit('serviceDiscovered', feature); if (feature.get('var') == Strophe.NS.MAM && prefs['default'] !== converse.message_archiving) { // Ask the server for archiving preferences converse.connection.sendIQ( diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index 825f819b7..8fceed668 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -6,6 +6,7 @@ Changelog * #439 auto_login and keepalive not working [jcbrand] * #440 null added as resource to contact [jcbrand] +* Add new event serviceDiscovered [jcbrand] 0.9.4 (2015-07-04) ------------------ diff --git a/docs/source/development.rst b/docs/source/development.rst index 6cfca806e..606e585b8 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -713,20 +713,6 @@ Here are the different events that are emitted: +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | Event Type | When is it triggered? | Example | +=================================+===================================================================================================+======================================================================================================+ -| **initialized** | Once converse.js has been initialized. | ``converse.listen.on('initialized', function (event) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **ready** | After connection has been established and converse.js has got all its ducks in a row. | ``converse.listen.on('ready', function (event) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **reconnect** | After the connection has dropped. Converse.js will attempt to reconnect when not in prebind mode. | ``converse.listen.on('reconnect', function (event) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **message** | When a message is received. | ``converse.listen.on('message', function (event, messageXML) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **messageSend** | When a message will be sent out. | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **noResumeableSession** | When keepalive=true but there aren't any stored prebind tokens. | ``converse.listen.on('noResumeableSession', function (event) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **roster** | When the roster is updated. | ``converse.listen.on('roster', function (event, items) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **callButtonClicked** | When a call button (i.e. with class .toggle-call) on a chat box has been clicked. | ``converse.listen.on('callButtonClicked', function (event, connection, model) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **chatBoxOpened** | When a chat box has been opened. | ``converse.listen.on('chatBoxOpened', function (event, chatbox) { ... });`` | @@ -739,17 +725,33 @@ Here are the different events that are emitted: +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **chatBoxToggled** | When a chat box has been minimized or maximized. | ``converse.listen.on('chatBoxToggled', function (event, chatbox) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **contactStatusChanged** | When a chat buddy's chat status has changed. | ``converse.listen.on('contactStatusChanged', function (event, buddy, status) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **contactStatusMessageChanged** | When a chat buddy's custom status message has changed. | ``converse.listen.on('contactStatusMessageChanged', function (event, buddy, messageText) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **message** | When a message is received. | ``converse.listen.on('message', function (event, messageXML) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **messageSend** | When a message will be sent out. | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **noResumeableSession** | When keepalive=true but there aren't any stored prebind tokens. | ``converse.listen.on('noResumeableSession', function (event) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **initialized** | Once converse.js has been initialized. | ``converse.listen.on('initialized', function (event) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **ready** | After connection has been established and converse.js has got all its ducks in a row. | ``converse.listen.on('ready', function (event) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **reconnect** | After the connection has dropped. Converse.js will attempt to reconnect when not in prebind mode. | ``converse.listen.on('reconnect', function (event) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **roomInviteSent** | After the user has sent out a direct invitation, to a roster contact, asking them to join a room. | ``converse.listen.on('roomInvite', function (event, roomview, invitee_jid, reason) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **roomInviteReceived** | After the user has sent out a direct invitation, to a roster contact, asking them to join a room. | ``converse.listen.on('roomInvite', function (event, roomview, invitee_jid, reason) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ +| **roster** | When the roster is updated. | ``converse.listen.on('roster', function (event, items) { ... });`` | ++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **statusChanged** | When own chat status has changed. | ``converse.listen.on('statusChanged', function (event, status) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **statusMessageChanged** | When own custom status message has changed. | ``converse.listen.on('statusMessageChanged', function (event, message) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **contactStatusChanged** | When a chat buddy's chat status has changed. | ``converse.listen.on('contactStatusChanged', function (event, buddy, status) { ... });`` | -+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **contactStatusMessageChanged** | When a chat buddy's custom status message has changed. | ``converse.listen.on('contactStatusMessageChanged', function (event, buddy, messageText) { ... });`` | +| **serviceDiscovered** | When converse.js has learned of a service provided by the XMPP server. See XEP-0030. | ``converse.listen.on('serviceDiscovered', function (service) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ diff --git a/spec/converse.js b/spec/converse.js index 093c130f0..7b4dcfba7 100644 --- a/spec/converse.js +++ b/spec/converse.js @@ -299,6 +299,10 @@ var box = converse_api.chats.open(jid); expect(box instanceof Object).toBeTruthy(); expect(box.get('box_id')).toBe(b64_sha1(jid)); + expect( + Object.keys(box), + ['close', 'endOTR', 'focus', 'get', 'initiateOTR', 'is_chatroom', 'maximize', 'minimize', 'open', 'set'] + ); var chatboxview = this.chatboxviews.get(jid); expect(chatboxview.$el.is(':visible')).toBeTruthy(); diff --git a/tests/main.js b/tests/main.js index ce3d52d40..1c4eafed7 100644 --- a/tests/main.js +++ b/tests/main.js @@ -60,6 +60,7 @@ require([ require([ "console-runner", "spec/converse", + "spec/disco", "spec/protocol", "spec/mam", "spec/otr", From 8a6bca191a0ff88a86762d3f32b3374adda1aa42 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 20:57:00 +0200 Subject: [PATCH 20/45] Small doc fixes. --- docs/source/configuration.rst | 4 ++-- docs/source/development.rst | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 1d1008a6a..2fefdcec8 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -328,8 +328,8 @@ See also: with regards to "stream resumption". -message_archives ----------------- +message_archiving +----------------- * Default: ``never`` diff --git a/docs/source/development.rst b/docs/source/development.rst index 606e585b8..268c49c87 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -209,6 +209,9 @@ Converse.js supports the *Message Archive Management* (`XEP-0313 `_) protocol, through which it is able to query an XMPP server for archived messages. +See also the **message_archiving** option in the :ref:`configuration-variables` section, which you'll usually +want to in conjunction with this API. + query ~~~~~ @@ -751,7 +754,7 @@ Here are the different events that are emitted: +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ | **statusMessageChanged** | When own custom status message has changed. | ``converse.listen.on('statusMessageChanged', function (event, message) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ -| **serviceDiscovered** | When converse.js has learned of a service provided by the XMPP server. See XEP-0030. | ``converse.listen.on('serviceDiscovered', function (service) { ... });`` | +| **serviceDiscovered** | When converse.js has learned of a service provided by the XMPP server. See XEP-0030. | ``converse.listen.on('serviceDiscovered', function (event, service) { ... });`` | +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+ From 36306c74189e4a91c2c403c5a515619b5b6ed401 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 23:13:43 +0200 Subject: [PATCH 21/45] Improve support for infinite scrolling of arhived messages. updates #306 --- converse.js | 52 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/converse.js b/converse.js index da2a9965d..ffb48d13e 100644 --- a/converse.js +++ b/converse.js @@ -1141,7 +1141,7 @@ this.save({'otr_status': UNENCRYPTED}); }, - createMessage: function ($message, $delay) { + createMessage: function ($message, $delay, archive_id) { $delay = $delay || $message.find('delay'); var body = $message.children('body').text(), delayed = $delay.length > 0, @@ -1179,15 +1179,16 @@ message: body || undefined, msgid: msgid, sender: sender, - time: time + time: time, + archive_id: archive_id }); }, - receiveMessage: function ($message, $delay) { + receiveMessage: function ($message, $delay, archive_id) { var $body = $message.children('body'); var text = ($body.length > 0 ? $body.text() : undefined); if ((!text) || (!converse.allow_otr)) { - return this.createMessage($message, $delay); + return this.createMessage($message, $delay, archive_id); } if (text.match(/^\?OTRv23?/)) { this.initiateOTR(text); @@ -1203,7 +1204,7 @@ } } else { // Normal unencrypted message. - this.createMessage($message, $delay); + this.createMessage($message, $delay, archive_id); } } } @@ -1236,7 +1237,6 @@ this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); // TODO check for changed fullname as well - this.model.on('change:archived_count', this.maybeFetchArchivedMessages.bind(this)); this.model.on('change:chat_state', this.sendChatState, this); this.model.on('change:chat_status', this.onChatStatusChanged, this); this.model.on('change:image', this.renderAvatar, this); @@ -1277,12 +1277,16 @@ }, onScroll: function (ev) { - var rsm = this.model.get('rsm'); - if (rsm && $(ev.target).scrollTop() === 0) { - if (! (rsm instanceof Strophe.RSM)) { - rsm = new Strophe.RSM(rsm); + var oldest; + if ($(ev.target).scrollTop() === 0) { + oldest = this.model.messages.where({'time': this.model.messages.pluck('time').sort()[0]}); + if (oldest) { + this.fetchArchivedMessages({ + 'before': oldest[0].get('archive_id'), + 'with': this.model.get('jid'), + 'max': converse.archived_messages_batch_size + }); } - this.fetchArchivedMessages(rsm.previous()); } }, @@ -1320,6 +1324,7 @@ // Whenever the archived_count attribute changes, // fetchArchivedMessages will be called. this.model.save({'archived_count': Number(attrs.count)}); + this.maybeFetchArchivedMessages(); }.bind(this), function (iq) { // On Error converse.log("Error occured while trying to fetch the archived messages count", "error"); @@ -1338,22 +1343,19 @@ } }, - fetchArchivedMessages: function (rsm) { + fetchArchivedMessages: function (options) { /* Fetch archived chat messages from the XMPP server. * * Then, upon receiving them, call onMessage on the chat box, * so that they are displayed inside it. */ API.archive.query( - rsm instanceof Strophe.RSM ? rsm : { + options || { 'before': '', // Page backwards from the most recent message 'with': this.model.get('jid'), 'max': converse.archived_messages_batch_size }, - function (messages, rsm) { - this.model.save({'rsm': rsm}); - _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes)); - }.bind(this), + _.partial(_.map, _, converse.chatboxes.onMessage.bind(converse.chatboxes)), _.partial(converse.log, "Error while trying to fetch archived messages", "error") ); }, @@ -1396,14 +1398,19 @@ match = text.match(/^\/(.*?)(?: (.*))?$/), fullname = this.model.get('fullname') || msg_dict.fullname, extra_classes = msg_dict.delayed && 'delayed' || '', + num_messages = this.model.messages.length, template, username, insertMessage; - if (this.model.messages.length && moment(msg_time).isBefore(this.model.messages.at(0).get('time'))) { + // FIXME: A better approach here is probably to look at what is + // already inside the content area, and from the determine if + // the message must be prepended or appended. + // That way we could probably also better show day indicators. + // That code should perhaps go into onMessageAdded + if (num_messages && msg_time.isBefore(this.model.messages.at(0).get('time'))) { insertMessage = $content.prepend.bind($content); } else { insertMessage = _.compose(this.scrollDown.bind(this), $content.append.bind($content)); } - if ((match) && (match[1] === 'me')) { text = text.replace(/^\/me/, ''); template = converse.templates.action; @@ -1960,7 +1967,7 @@ this.initDragResize(); } this.setChatState(ACTIVE); - return this.focus(); + return this.scrollDown().focus(); }, scrollDown: function () { @@ -3429,7 +3436,8 @@ chatbox, resource, roster_item, from_jid = $message.attr('from'), to_jid = $message.attr('to'), - to_resource = Strophe.getResourceFromJid(to_jid); + to_resource = Strophe.getResourceFromJid(to_jid), + archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id'); if (to_resource && to_resource !== converse.resource) { converse.log('Ignore incoming message intended for a different resource: '+to_jid, 'info'); @@ -3468,7 +3476,7 @@ if (!this.isOnlyChatStateNotification($message) && !is_me) { converse.playNotification(); } - chatbox.receiveMessage($message, $delay); + chatbox.receiveMessage($message, $delay, archive_id); converse.roster.addResource(contact_jid, resource); converse.emit('message', message); return true; From 3b2b4ed6c774a6dec70295b940fd3439175363b6 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 17 Jul 2015 23:46:02 +0200 Subject: [PATCH 22/45] Some improvements to scrolling. --- converse.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/converse.js b/converse.js index ffb48d13e..944ee6586 100644 --- a/converse.js +++ b/converse.js @@ -1399,6 +1399,7 @@ fullname = this.model.get('fullname') || msg_dict.fullname, extra_classes = msg_dict.delayed && 'delayed' || '', num_messages = this.model.messages.length, + has_scrollbar = $content.get(0).scrollHeight > $content[0].clientHeight, template, username, insertMessage; // FIXME: A better approach here is probably to look at what is @@ -1407,7 +1408,11 @@ // That way we could probably also better show day indicators. // That code should perhaps go into onMessageAdded if (num_messages && msg_time.isBefore(this.model.messages.at(0).get('time'))) { - insertMessage = $content.prepend.bind($content); + if (! has_scrollbar || $content.scrollTop() !== 0) { + insertMessage = _.compose(this.scrollDown.bind(this), $content.prepend.bind($content)); + } else { + insertMessage = $content.prepend.bind($content); + } } else { insertMessage = _.compose(this.scrollDown.bind(this), $content.append.bind($content)); } @@ -1495,7 +1500,7 @@ converse.incrementMsgCounter(); } if (!this.model.get('minimized') && !this.$el.is(':visible')) { - this.show(); + _.debounce(this.show, 100); } }, @@ -1959,15 +1964,21 @@ if (this.$el.is(':visible') && this.$el.css('opacity') == "1") { return this.focus(); } - this.$el.fadeIn(callback); - if (converse.connection.connected) { - // Without a connection, we haven't yet initialized - // localstorage - this.model.save(); - this.initDragResize(); - } - this.setChatState(ACTIVE); - return this.scrollDown().focus(); + this.$el.fadeIn(function () { + if (typeof callback == "function") { + callback.apply(this, arguments); + } + if (converse.connection.connected) { + // Without a connection, we haven't yet initialized + // localstorage + this.model.save(); + this.initDragResize(); + } + this.setChatState(ACTIVE); + this.scrollDown().focus(); + }.bind(this) + ); + return this; }, scrollDown: function () { From 796582d96f72c09ca1027a05f684a976e3be391f Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 18 Jul 2015 00:08:21 +0200 Subject: [PATCH 23/45] Refactoring: got rid of the maybeFetchArchivedMessages method. updates #306 --- converse.js | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/converse.js b/converse.js index 944ee6586..9efb67708 100644 --- a/converse.js +++ b/converse.js @@ -1313,7 +1313,7 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { return; } - if (typeof this.model.get('archived_count') == 'undefined') { + if (this.model.messages.length < converse.archived_messages_batch_size) { // Get the amount of archived messages // Refer to: https://xmpp.org/extensions/xep-0059.html#count API.archive.query({ @@ -1321,25 +1321,18 @@ 'max': 0 }, function (messages, attrs) { // On Success - // Whenever the archived_count attribute changes, - // fetchArchivedMessages will be called. - this.model.save({'archived_count': Number(attrs.count)}); - this.maybeFetchArchivedMessages(); + if (this.model.messages.length < Number(attrs.count)) { + this.fetchArchivedMessages({ + 'before': '', // Page backwards from the most recent message + 'with': this.model.get('jid'), + 'max': converse.archived_messages_batch_size + }); + } }.bind(this), function (iq) { // On Error converse.log("Error occured while trying to fetch the archived messages count", "error"); - this.model.save({'archived_count': 0}); }.bind(this) ); - } else { - this.maybeFetchArchivedMessages(); - } - }, - - maybeFetchArchivedMessages: function () { - if (this.model.messages.length < this.model.get('archived_count') && - this.model.messages.length < converse.archived_messages_batch_size) { - this.fetchArchivedMessages(); } }, @@ -1349,12 +1342,7 @@ * Then, upon receiving them, call onMessage on the chat box, * so that they are displayed inside it. */ - API.archive.query( - options || { - 'before': '', // Page backwards from the most recent message - 'with': this.model.get('jid'), - 'max': converse.archived_messages_batch_size - }, + API.archive.query(options, _.partial(_.map, _, converse.chatboxes.onMessage.bind(converse.chatboxes)), _.partial(converse.log, "Error while trying to fetch archived messages", "error") ); From ec02d34af9771a819d5e85ba0f63b0643eafb2b5 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 18 Jul 2015 09:20:13 +0200 Subject: [PATCH 24/45] Improvement. Don't lose your place with infinite upscroll. updates #306 --- converse.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/converse.js b/converse.js index 9efb67708..997f2465f 100644 --- a/converse.js +++ b/converse.js @@ -1396,13 +1396,15 @@ // That way we could probably also better show day indicators. // That code should perhaps go into onMessageAdded if (num_messages && msg_time.isBefore(this.model.messages.at(0).get('time'))) { - if (! has_scrollbar || $content.scrollTop() !== 0) { - insertMessage = _.compose(this.scrollDown.bind(this), $content.prepend.bind($content)); - } else { - insertMessage = $content.prepend.bind($content); - } + insertMessage = _.compose( + this.scrollDownMessageHeight.bind(this), + function ($el) { + $content.prepend($el); + return $el; + } + ); } else { - insertMessage = _.compose(this.scrollDown.bind(this), $content.append.bind($content)); + insertMessage = _.compose(_.debounce(this.scrollDown.bind(this), 50), $content.append.bind($content)); } if ((match) && (match[1] === 'me')) { text = text.replace(/^\/me/, ''); @@ -1969,6 +1971,14 @@ return this; }, + scrollDownMessageHeight: function ($message) { + var $content = this.$('.chat-content'); + if ($content.is(':visible')) { + $content.scrollTop($content.scrollTop() + $message[0].scrollHeight); + } + return this; + }, + scrollDown: function () { var $content = this.$('.chat-content'); if ($content.is(':visible')) { From 1a7a5e80544814451089dd5dee9957eb8f92cc61 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 18 Jul 2015 21:39:49 +0200 Subject: [PATCH 25/45] Add a spinner when loading during infinite scrolling. updates #306 --- converse.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/converse.js b/converse.js index 997f2465f..e5d580381 100644 --- a/converse.js +++ b/converse.js @@ -1281,6 +1281,7 @@ if ($(ev.target).scrollTop() === 0) { oldest = this.model.messages.where({'time': this.model.messages.pluck('time').sort()[0]}); if (oldest) { + this.$el.find('.chat-content').prepend(''); this.fetchArchivedMessages({ 'before': oldest[0].get('archive_id'), 'with': this.model.get('jid'), @@ -1343,7 +1344,12 @@ * so that they are displayed inside it. */ API.archive.query(options, - _.partial(_.map, _, converse.chatboxes.onMessage.bind(converse.chatboxes)), + function (messages) { + this.clearSpinner(); + if (messages.length) { + _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes)); + } + }.bind(this), _.partial(converse.log, "Error while trying to fetch archived messages", "error") ); }, @@ -1379,6 +1385,13 @@ return this; }, + clearSpinner: function () { + var $content = this.$el.find('.chat-content'); + if ($content.children(':first').is('span.spinner')) { + $content.children(':first').first().remove(); + } + }, + showMessage: function (msg_dict) { var $content = this.$el.find('.chat-content'), msg_time = moment(msg_dict.time) || moment, From 6bd0037cbb73c69b515f85d88c723200aef274e2 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 19 Jul 2015 12:08:43 +0200 Subject: [PATCH 26/45] Save the chat content area on the view. To reduce $.find queries. --- converse.js | 72 +++++++++++++++++++++++------------------------------ 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/converse.js b/converse.js index e5d580381..de4ab6c5d 100644 --- a/converse.js +++ b/converse.js @@ -1269,8 +1269,9 @@ ) ) ); + this.$content = this.$el.find('.chat-content'); this.renderToolbar().renderAvatar(); - this.$el.find('.chat-content').on('scroll', _.debounce(this.onScroll.bind(this), 100)); + this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100)); converse.emit('chatBoxOpened', this); setTimeout(converse.refreshWebkit, 50); return this.showStatusMessage(); @@ -1281,7 +1282,7 @@ if ($(ev.target).scrollTop() === 0) { oldest = this.model.messages.where({'time': this.model.messages.pluck('time').sort()[0]}); if (oldest) { - this.$el.find('.chat-content').prepend(''); + this.$content.prepend(''); this.fetchArchivedMessages({ 'before': oldest[0].get('archive_id'), 'with': this.model.get('jid'), @@ -1368,11 +1369,10 @@ }, showStatusNotification: function (message, keep_old) { - var $chat_content = this.$el.find('.chat-content'); if (!keep_old) { - $chat_content.find('div.chat-event').remove(); + this.$content.find('div.chat-event').remove(); } - $chat_content.append($('
').text(message)); + this.$content.append($('
').text(message)); this.scrollDown(); }, @@ -1380,27 +1380,25 @@ if (typeof ev !== "undefined") { ev.stopPropagation(); } var result = confirm(__("Are you sure you want to clear the messages from this room?")); if (result === true) { - this.$el.find('.chat-content').empty(); + this.$content.empty(); } return this; }, clearSpinner: function () { - var $content = this.$el.find('.chat-content'); - if ($content.children(':first').is('span.spinner')) { - $content.children(':first').first().remove(); + if (this.$content.children(':first').is('span.spinner')) { + this.$content.children(':first').first().remove(); } }, showMessage: function (msg_dict) { - var $content = this.$el.find('.chat-content'), - msg_time = moment(msg_dict.time) || moment, + var msg_time = moment(msg_dict.time) || moment, text = msg_dict.message, match = text.match(/^\/(.*?)(?: (.*))?$/), fullname = this.model.get('fullname') || msg_dict.fullname, extra_classes = msg_dict.delayed && 'delayed' || '', num_messages = this.model.messages.length, - has_scrollbar = $content.get(0).scrollHeight > $content[0].clientHeight, + has_scrollbar = this.$content.get(0).scrollHeight > this.$content[0].clientHeight, template, username, insertMessage; // FIXME: A better approach here is probably to look at what is @@ -1412,12 +1410,12 @@ insertMessage = _.compose( this.scrollDownMessageHeight.bind(this), function ($el) { - $content.prepend($el); + this.$content.prepend($el); return $el; - } + }.bind(this) ); } else { - insertMessage = _.compose(_.debounce(this.scrollDown.bind(this), 50), $content.append.bind($content)); + insertMessage = _.compose(_.debounce(this.scrollDown.bind(this), 50), this.$content.append.bind(this.$content)); } if ((match) && (match[1] === 'me')) { text = text.replace(/^\/me/, ''); @@ -1427,7 +1425,7 @@ template = converse.templates.message; username = msg_dict.sender === 'me' && __('me') || fullname; } - $content.find('div.chat-event').remove(); + this.$content.find('div.chat-event').remove(); if (this.is_chatroom && msg_dict.sender == 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) { // Add special class to mark groupchat messages in which we @@ -1450,15 +1448,14 @@ }, showHelpMessages: function (msgs, type, spinner) { - var $chat_content = this.$el.find('.chat-content'), i, - msgs_length = msgs.length; + var i, msgs_length = msgs.length; for (i=0; i'+msgs[i]+'')); + this.$content.append($('
'+msgs[i]+'
')); } if (spinner === true) { - $chat_content.append(''); + this.$content.append(''); } else if (spinner === false) { - $chat_content.find('span.spinner').remove(); + this.$content.find('span.spinner').remove(); } return this.scrollDown(); }, @@ -1476,7 +1473,7 @@ prev_date = moment(previous_message.get('time')); if (prev_date.isBefore(time, 'day')) { this_date = moment(time); - this.$el.find('.chat-content').append(converse.templates.new_day({ + this.$content.append(converse.templates.new_day({ isodate: this_date.format("YYYY-MM-DD"), datestring: this_date.format("dddd MMM Do YYYY") })); @@ -1490,7 +1487,7 @@ this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing')); return; } else if (_.contains([INACTIVE, ACTIVE], message.get('chat_state'))) { - this.$el.find('.chat-content div.chat-event').remove(); + this.$content.find('div.chat-event').remove(); return; } else if (message.get('chat_state') === GONE) { this.showStatusNotification(message.get('fullname')+' '+__('has gone away')); @@ -1673,7 +1670,7 @@ if (ev && ev.preventDefault) { ev.preventDefault(); } var result = confirm(__("Are you sure you want to clear the messages from this chat box?")); if (result === true) { - this.$el.find('.chat-content').empty(); + this.$content.empty(); this.model.messages.reset(); this.model.messages.browserStorage._clear(); } @@ -1985,17 +1982,15 @@ }, scrollDownMessageHeight: function ($message) { - var $content = this.$('.chat-content'); - if ($content.is(':visible')) { - $content.scrollTop($content.scrollTop() + $message[0].scrollHeight); + if (this.$content.is(':visible')) { + this.$content.scrollTop(this.$content.scrollTop() + $message[0].scrollHeight); } return this; }, scrollDown: function () { - var $content = this.$('.chat-content'); - if ($content.is(':visible')) { - $content.scrollTop($content[0].scrollHeight); + if (this.$content.is(':visible')) { + this.$content.scrollTop(this.$content[0].scrollHeight); } return this; } @@ -2749,6 +2744,7 @@ })) .append(this.occupantsview.render().$el); this.renderToolbar(); + this.$content = this.$el.find('.chat-content'); } // XXX: This is a bit of a hack, to make sure that the // sidebar's state is remembered. @@ -2767,16 +2763,12 @@ this.model.save({hidden_occupants: true}); $el.removeClass('icon-hide-users').addClass('icon-show-users'); this.$('form.sendXMPPMessage, .chat-area').animate({width: '100%'}); - this.$('div.participants').animate({width: 0}, function () { - this.scrollDown(); - }.bind(this)); + this.$('div.participants').animate({width: 0}, this.scrollDown.bind(this)); } else { this.model.save({hidden_occupants: false}); $el.removeClass('icon-show-users').addClass('icon-hide-users'); this.$('.chat-area, form.sendXMPPMessage').css({width: ''}); - this.$('div.participants').show().animate({width: 'auto'}, function () { - this.scrollDown(); - }.bind(this)); + this.$('div.participants').show().animate({width: 'auto'}, this.scrollDown.bind(this)); } }, @@ -3213,8 +3205,7 @@ * Allow user to configure chat room if they are the owner. * See: http://xmpp.org/registrar/mucstatus.html */ - var $chat_content, - disconnect_msgs = [], + var disconnect_msgs = [], msgs = [], reasons = []; $el.find('x[xmlns="'+Strophe.NS.MUC_USER+'"]').each(function (idx, x) { @@ -3258,9 +3249,8 @@ this.model.set('connection_status', Strophe.Status.DISCONNECTED); return; } - $chat_content = this.$el.find('.chat-content'); for (i=0; i Date: Sun, 19 Jul 2015 12:10:22 +0200 Subject: [PATCH 27/45] Store the isodate on chat messages in the DOM. This will be useful later when we want to better place messages in the right order. --- converse.js | 1 + src/templates/action.html | 2 +- src/templates/message.html | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/converse.js b/converse.js index de4ab6c5d..7b47c43ab 100644 --- a/converse.js +++ b/converse.js @@ -1435,6 +1435,7 @@ var message = template({ 'sender': msg_dict.sender, 'time': msg_time.format('hh:mm'), + 'isodate': msg_time, 'username': username, 'message': '', 'extra_classes': extra_classes diff --git a/src/templates/action.html b/src/templates/action.html index b2e9e7904..107d28c01 100644 --- a/src/templates/action.html +++ b/src/templates/action.html @@ -1,4 +1,4 @@ -
+
{{time}} **{{username}} {{message}}
diff --git a/src/templates/message.html b/src/templates/message.html index 5ab15ea2f..b36e6e5bf 100644 --- a/src/templates/message.html +++ b/src/templates/message.html @@ -1,4 +1,4 @@ -
+
{{time}} {{username}}:  {{message}}
From 06a6d6c4d0f7ce96d5db9c610c959e02c9136d11 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 19 Jul 2015 12:22:10 +0200 Subject: [PATCH 28/45] Refactor out code for showing a new day in the chat box. --- converse.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/converse.js b/converse.js index 7b47c43ab..7e7a498a5 100644 --- a/converse.js +++ b/converse.js @@ -1233,7 +1233,7 @@ }, initialize: function () { - this.model.messages.on('add', this.onMessageAdded, this); + this.model.messages.on('add', _.compose(this.onMessageAdded, this.showNewDay), this); this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); // TODO check for changed fullname as well @@ -1461,17 +1461,15 @@ return this.scrollDown(); }, - onMessageAdded: function (message) { + showNewDay: function (message) { + /* If this message is on a different day than the one received + * prior, then indicate it on the chatbox. + */ var time = message.get('time'), - times = this.model.messages.pluck('time'), - previous_message, idx, this_date, prev_date, text, match; - - // If this message is on a different day than the one received - // prior, then indicate it on the chatbox. - idx = _.indexOf(times, time)-1; + idx = _.indexOf(this.model.messages.pluck('time'), time)-1, + this_date, prev_date; if (idx >= 0) { - previous_message = this.model.messages.at(idx); - prev_date = moment(previous_message.get('time')); + prev_date = moment(this.model.messages.at(idx).get('time')); if (prev_date.isBefore(time, 'day')) { this_date = moment(time); this.$content.append(converse.templates.new_day({ @@ -1480,6 +1478,10 @@ })); } } + return message; + }, + + onMessageAdded: function (message) { if (!message.get('message')) { if (message.get('chat_state') === COMPOSING) { this.showStatusNotification(message.get('fullname')+' '+__('is typing')); @@ -2692,7 +2694,7 @@ is_chatroom: true, initialize: function () { - this.model.messages.on('add', this.onMessageAdded, this); + this.model.messages.on('add', _.compose(this.onMessageAdded, this.showNewDay), this); this.model.on('change:minimized', function (item) { if (item.get('minimized')) { this.hide(); From e8fba4a7d080e2f8d89919d4261537167ccc56e4 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 19 Jul 2015 13:43:26 +0200 Subject: [PATCH 29/45] Refactored showNewDay to indicate day changes between messages. updates #306 --- converse.js | 58 +++++++++++++++++++++++--------------- spec/chatbox.js | 2 +- src/templates/new_day.html | 2 +- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/converse.js b/converse.js index 7e7a498a5..2afebe07a 100644 --- a/converse.js +++ b/converse.js @@ -1387,7 +1387,7 @@ clearSpinner: function () { if (this.$content.children(':first').is('span.spinner')) { - this.$content.children(':first').first().remove(); + this.$content.children(':first').remove(); } }, @@ -1401,11 +1401,9 @@ has_scrollbar = this.$content.get(0).scrollHeight > this.$content[0].clientHeight, template, username, insertMessage; - // FIXME: A better approach here is probably to look at what is - // already inside the content area, and from the determine if - // the message must be prepended or appended. - // That way we could probably also better show day indicators. - // That code should perhaps go into onMessageAdded + // TODO: A better approach here is probably to look at what is + // already inside the content area, and from that determine if + // the message must be prepended or appended. Similar to showNewDay if (num_messages && msg_time.isBefore(this.model.messages.at(0).get('time'))) { insertMessage = _.compose( this.scrollDownMessageHeight.bind(this), @@ -1435,7 +1433,7 @@ var message = template({ 'sender': msg_dict.sender, 'time': msg_time.format('hh:mm'), - 'isodate': msg_time, + 'isodate': msg_time.format(), 'username': username, 'message': '', 'extra_classes': extra_classes @@ -1462,21 +1460,35 @@ }, showNewDay: function (message) { - /* If this message is on a different day than the one received - * prior, then indicate it on the chatbox. + /* Messages may be received chronologically, from old to new or + * new to old. + * + * If this message is older than the oldest, or newer then the + * newest, we show a new day indication in the chat content + * area. + * + * Parameters: + * (XMLElement) message - The message stanza received from the XMPP server. */ - var time = message.get('time'), - idx = _.indexOf(this.model.messages.pluck('time'), time)-1, - this_date, prev_date; - if (idx >= 0) { - prev_date = moment(this.model.messages.at(idx).get('time')); - if (prev_date.isBefore(time, 'day')) { - this_date = moment(time); - this.$content.append(converse.templates.new_day({ - isodate: this_date.format("YYYY-MM-DD"), - datestring: this_date.format("dddd MMM Do YYYY") - })); - } + var first_message_date = this.$content.children(':first').data('isodate'); + if (typeof(first_message_date) == "undefined") { + return message; + } + var last_message_date = this.$content.children(':last').data('isodate'); + var this_date = moment(message.get('time')); + var day_date; + if (this_date.isBefore(first_message_date, 'day')) { + day_date = moment(first_message_date).startOf('day'); + this.$content.prepend(converse.templates.new_day({ + isodate: day_date.format(), + datestring: day_date.format("dddd MMM Do YYYY") + })); + } else if (this_date.isAfter(last_message_date, 'day')) { + day_date = moment(this_date).startOf('day'); + this.$content.append(converse.templates.new_day({ + isodate: this_date.format(), + datestring: this_date.format("dddd MMM Do YYYY") + })); } return message; }, @@ -1513,8 +1525,7 @@ * Parameters: * (string) text - The chat message text. */ - // TODO: We might want to send to specfic resources. Especially - // in the OTR case. + // TODO: We might want to send to specfic resources. Especially in the OTR case. var timestamp = (new Date()).getTime(); var bare_jid = this.model.get('jid'); var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp}) @@ -4258,6 +4269,7 @@ onRosterPush: function (iq) { /* Handle roster updates from the XMPP server. * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push + * * Parameters: * (XMLElement) IQ - The IQ stanza received from the XMPP server. */ diff --git a/spec/chatbox.js b/spec/chatbox.js index db9a30ef9..e2d5f60fa 100644 --- a/spec/chatbox.js +++ b/spec/chatbox.js @@ -652,7 +652,7 @@ var message_date = new Date(); expect($time.length).toEqual(1); expect($time.attr('class')).toEqual('chat-date'); - expect($time.attr('datetime')).toEqual(moment(message_date).format("YYYY-MM-DD")); + expect($time.data('isodate')).toEqual(moment(message_date).format()); expect($time.text()).toEqual(moment(message_date).format("dddd MMM Do YYYY")); // Normal checks for the 2nd message diff --git a/src/templates/new_day.html b/src/templates/new_day.html index 119132622..56968ca03 100644 --- a/src/templates/new_day.html +++ b/src/templates/new_day.html @@ -1 +1 @@ - + From 7ff38896e91f6e70a876986568cbc3c5ebe0e641 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 19 Jul 2015 23:23:02 +0200 Subject: [PATCH 30/45] Refactored the code showing messages and day indicators. Fixes the issue where a topmost day indicator wasn't shown when doing infinite scrolling upwards. --- converse.js | 200 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 116 insertions(+), 84 deletions(-) diff --git a/converse.js b/converse.js index 2afebe07a..3a6f8e1ba 100644 --- a/converse.js +++ b/converse.js @@ -968,7 +968,8 @@ this.Message = Backbone.Model; this.Messages = Backbone.Collection.extend({ - model: converse.Message + model: converse.Message, + comparator: 'time' }); this.ChatBox = Backbone.Model.extend({ @@ -1233,7 +1234,7 @@ }, initialize: function () { - this.model.messages.on('add', _.compose(this.onMessageAdded, this.showNewDay), this); + this.model.messages.on('add', this.onMessageAdded, this); this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); // TODO check for changed fullname as well @@ -1278,17 +1279,13 @@ }, onScroll: function (ev) { - var oldest; - if ($(ev.target).scrollTop() === 0) { - oldest = this.model.messages.where({'time': this.model.messages.pluck('time').sort()[0]}); - if (oldest) { - this.$content.prepend(''); - this.fetchArchivedMessages({ - 'before': oldest[0].get('archive_id'), - 'with': this.model.get('jid'), - 'max': converse.archived_messages_batch_size - }); - } + if ($(ev.target).scrollTop() === 0 && this.model.messages.length) { + this.$content.prepend(''); + this.fetchArchivedMessages({ + 'before': this.model.messages.at(0).get('archive_id'), + 'with': this.model.get('jid'), + 'max': converse.archived_messages_batch_size + }); } }, @@ -1391,59 +1388,123 @@ } }, - showMessage: function (msg_dict) { - var msg_time = moment(msg_dict.time) || moment, - text = msg_dict.message, - match = text.match(/^\/(.*?)(?: (.*))?$/), - fullname = this.model.get('fullname') || msg_dict.fullname, - extra_classes = msg_dict.delayed && 'delayed' || '', - num_messages = this.model.messages.length, - has_scrollbar = this.$content.get(0).scrollHeight > this.$content[0].clientHeight, - template, username, insertMessage; + prependDayIndicator: function (date) { + /* Prepends an indicator into the chat area, showing the day as + * given by the passed in date. + * + * Parameters: + * (String) date - An ISO8601 date string. + */ + var day_date = moment(date).startOf('day'); + this.$content.prepend(converse.templates.new_day({ + isodate: day_date.format(), + datestring: day_date.format("dddd MMM Do YYYY") + })); + }, - // TODO: A better approach here is probably to look at what is - // already inside the content area, and from that determine if - // the message must be prepended or appended. Similar to showNewDay - if (num_messages && msg_time.isBefore(this.model.messages.at(0).get('time'))) { - insertMessage = _.compose( - this.scrollDownMessageHeight.bind(this), - function ($el) { - this.$content.prepend($el); - return $el; - }.bind(this) - ); - } else { - insertMessage = _.compose(_.debounce(this.scrollDown.bind(this), 50), this.$content.append.bind(this.$content)); + showMessage: function (attrs) { + /* Inserts a chat message into the content area of the chat box. + * Will also insert a new day indicator if the message is on a + * different day. + * + * The message to show may either be newer than the newest + * message, or older than the oldest message. + * + * Parameters: + * (Object) attrs: An object containing the message attributes. + */ + var current_msg_date = moment(attrs.time) || moment, + $first_msg = this.$content.children('.chat-message:first'), + first_msg_date = $first_msg.data('isodate'), + last_msg_date = this.$content.children(':last').data('isodate'), + day_date; + + if (typeof first_msg_date !== "undefined" && + (current_msg_date.isBefore(first_msg_date) || + (current_msg_date.isSame(first_msg_date) && !current_msg_date.isSame(last_msg_date)))) { + + if ($first_msg.prev().length === 0) { + // There's no day indicator before the first message, so we prepend one. + this.prependDayIndicator(first_msg_date); + } + if (current_msg_date.isBefore(first_msg_date, 'day')) { + _.compose( + this.scrollDownMessageHeight.bind(this), + function ($el) { + this.$content.prepend($el); + return $el; + }.bind(this) + )(this.renderMessage(attrs)); + // This message is on a different day, so we add a day indicator. + this.prependDayIndicator(current_msg_date); + } else { + // The message is before the first, but on the same day. + // We need to prepend the message immediately before the + // first message (so that it'll still be after the day indicator). + _.compose( + this.scrollDownMessageHeight.bind(this), + function ($el) { + $el.insertBefore($first_msg); + return $el; + } + )(this.renderMessage(attrs)); + } + return; + } else if (current_msg_date.isAfter(last_msg_date, 'day')) { + // Append a new day indicator + day_date = moment(current_msg_date).startOf('day'); + this.$content.append(converse.templates.new_day({ + isodate: current_msg_date.format(), + datestring: current_msg_date.format("dddd MMM Do YYYY") + })); } + _.compose( + _.debounce(this.scrollDown.bind(this), 50), + this.$content.append.bind(this.$content) + )(this.renderMessage(attrs)); + }, + + renderMessage: function (attrs) { + /* Renders a chat message based on the passed in attributes. + * + * Parameters: + * (Object) attrs: An object containing the message attributes. + * + * Returns: + * The DOM element representing the message. + */ + var msg_time = moment(attrs.time) || moment, + text = attrs.message, + match = text.match(/^\/(.*?)(?: (.*))?$/), + fullname = this.model.get('fullname') || attrs.fullname, + extra_classes = attrs.delayed && 'delayed' || '', + template, username; + if ((match) && (match[1] === 'me')) { text = text.replace(/^\/me/, ''); template = converse.templates.action; username = fullname; } else { template = converse.templates.message; - username = msg_dict.sender === 'me' && __('me') || fullname; + username = attrs.sender === 'me' && __('me') || fullname; } this.$content.find('div.chat-event').remove(); - if (this.is_chatroom && msg_dict.sender == 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) { + if (this.is_chatroom && attrs.sender == 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) { // Add special class to mark groupchat messages in which we // are mentioned. extra_classes += ' mentioned'; } - var message = template({ - 'sender': msg_dict.sender, - 'time': msg_time.format('hh:mm'), - 'isodate': msg_time.format(), - 'username': username, - 'message': '', - 'extra_classes': extra_classes - }); - insertMessage( - $(message).children('.chat-message-content').first().text(text) + return $(template({ + 'sender': attrs.sender, + 'time': msg_time.format('hh:mm'), + 'isodate': msg_time.format(), + 'username': username, + 'message': '', + 'extra_classes': extra_classes + })).children('.chat-message-content').first().text(text) .addHyperlinks() - .addEmoticons(converse.visible_toolbar_buttons.emoticons) - .parent() - ); + .addEmoticons(converse.visible_toolbar_buttons.emoticons).parent(); }, showHelpMessages: function (msgs, type, spinner) { @@ -1459,41 +1520,12 @@ return this.scrollDown(); }, - showNewDay: function (message) { - /* Messages may be received chronologically, from old to new or - * new to old. - * - * If this message is older than the oldest, or newer then the - * newest, we show a new day indication in the chat content - * area. + onMessageAdded: function (message) { + /* Handler that gets called when a new message object is created. * * Parameters: - * (XMLElement) message - The message stanza received from the XMPP server. + * (Object) message - The message Backbone object that was added. */ - var first_message_date = this.$content.children(':first').data('isodate'); - if (typeof(first_message_date) == "undefined") { - return message; - } - var last_message_date = this.$content.children(':last').data('isodate'); - var this_date = moment(message.get('time')); - var day_date; - if (this_date.isBefore(first_message_date, 'day')) { - day_date = moment(first_message_date).startOf('day'); - this.$content.prepend(converse.templates.new_day({ - isodate: day_date.format(), - datestring: day_date.format("dddd MMM Do YYYY") - })); - } else if (this_date.isAfter(last_message_date, 'day')) { - day_date = moment(this_date).startOf('day'); - this.$content.append(converse.templates.new_day({ - isodate: this_date.format(), - datestring: this_date.format("dddd MMM Do YYYY") - })); - } - return message; - }, - - onMessageAdded: function (message) { if (!message.get('message')) { if (message.get('chat_state') === COMPOSING) { this.showStatusNotification(message.get('fullname')+' '+__('is typing')); @@ -1615,7 +1647,7 @@ * * Parameters: * (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE) - * (no_save) no_save - Just do the cleanup or setup but don't actually save the state. + * (Boolean) no_save - Just do the cleanup or setup but don't actually save the state. */ if (typeof this.chat_state_timeout !== 'undefined') { clearTimeout(this.chat_state_timeout); @@ -2705,7 +2737,7 @@ is_chatroom: true, initialize: function () { - this.model.messages.on('add', _.compose(this.onMessageAdded, this.showNewDay), this); + this.model.messages.on('add', this.onMessageAdded, this); this.model.on('change:minimized', function (item) { if (item.get('minimized')) { this.hide(); From ccf908a65856294bff444038501e705417010bf6 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 19 Jul 2015 23:36:26 +0200 Subject: [PATCH 31/45] Document that previous and next RSM methods take limit parameter. updates #306 --- docs/source/development.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/source/development.rst b/docs/source/development.rst index 268c49c87..9dbd9c098 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -304,7 +304,8 @@ as two utility methods ``next``, and ``previous``. When you call one of these utility methods on the returned RSM object, and then pass the result into a new query, you'll receive the next or previous batch of -archived messages. +archived messages. Please note, when calling these methods, pass in an integer +to limit your results. .. code-block:: javascript @@ -312,7 +313,7 @@ archived messages. // Do something with the messages, like showing them in your webpage. // ... // You can now use the returned "rsm" object, to fetch the next batch of messages: - converse.archive.query(rsm.next(), callback, errback)) + converse.archive.query(rsm.next(10), callback, errback)) } converse.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); @@ -330,10 +331,8 @@ message, pass in the ``before`` parameter with an empty string value ``''``. // Do something with the messages, like showing them in your webpage. // ... // You can now use the returned "rsm" object, to fetch the previous batch of messages: - rsm.previous(); // Call previous method, to update the object - // parameters so that the previous batch of messages will be returned. - rsm.count = 10; // Increase the page size (currently it's set to 1, - // because that was what we specified in our last query. + rsm.previous(5); // Call previous method, to update the object's parameters, + // passing in a limit value of 5. // Now we query again, to get the previous batch. converse.archive.query(rsm, callback, errback); } From 1675109e71e12c2b2e4f676b02b86a5982884c53 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 19 Jul 2015 23:54:45 +0200 Subject: [PATCH 32/45] Two changes, see below. - Don't play a sound notification for forwarded messages. - Bugfix. Return when no chat box returned. --- converse.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/converse.js b/converse.js index 3a6f8e1ba..db43c846b 100644 --- a/converse.js +++ b/converse.js @@ -3527,11 +3527,13 @@ } // Get chat box, but only create a new one when the message has a body. chatbox = this.getChatBox(contact_jid, $message.find('body').length > 0); - + if (!chatbox) { + return true; + } if (msgid && chatbox.messages.findWhere({msgid: msgid})) { return true; // We already have this message stored. } - if (!this.isOnlyChatStateNotification($message) && !is_me) { + if (!this.isOnlyChatStateNotification($message) && !is_me && !$forwarded.length) { converse.playNotification(); } chatbox.receiveMessage($message, $delay, archive_id); From 79fba8a6c620a5dca7d232b07d2b371408dce60e Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 20 Jul 2015 09:54:56 +0200 Subject: [PATCH 33/45] Bugfix. Didn't call debounced method. Causing the chat box to not appear when a new message is received. --- converse.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/converse.js b/converse.js index db43c846b..87a71943d 100644 --- a/converse.js +++ b/converse.js @@ -1547,7 +1547,7 @@ converse.incrementMsgCounter(); } if (!this.model.get('minimized') && !this.$el.is(':visible')) { - _.debounce(this.show, 100); + _.debounce(this.show.bind(this), 100)(); } }, @@ -2022,8 +2022,7 @@ } this.setChatState(ACTIVE); this.scrollDown().focus(); - }.bind(this) - ); + }.bind(this)); return this; }, From 388012f2cd88162897abb5a485d44f0b9fc57cf1 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 20 Jul 2015 09:57:28 +0200 Subject: [PATCH 34/45] Workaround for ejabberd... which doesn't support getting the item count of archived messages. See here: https://xmpp.org/extensions/xep-0059.html#count So we don't first fetch a count of archived messages instead try to fetch the messages themselves immediately. updates #306 --- converse.js | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/converse.js b/converse.js index 87a71943d..9625e49e5 100644 --- a/converse.js +++ b/converse.js @@ -1297,42 +1297,20 @@ */ this.model.messages.fetch({ 'add': true, - 'success': this.afterFetchingCachedMessages.bind(this) - }); - return this; - }, - - afterFetchingCachedMessages: function () { - /* Handler method, called after messages cached in - * sessionStorage have been fetched. - * - * The goal of this method is to determine how many archived - * messages exist and whether we should fetch them or not. - */ - if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { - return; - } - if (this.model.messages.length < converse.archived_messages_batch_size) { - // Get the amount of archived messages - // Refer to: https://xmpp.org/extensions/xep-0059.html#count - API.archive.query({ - 'with': this.model.get('jid'), - 'max': 0 - }, - function (messages, attrs) { // On Success - if (this.model.messages.length < Number(attrs.count)) { + 'success': function () { + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + return; + } + if (this.model.messages.length < converse.archived_messages_batch_size) { this.fetchArchivedMessages({ 'before': '', // Page backwards from the most recent message 'with': this.model.get('jid'), 'max': converse.archived_messages_batch_size }); } - }.bind(this), - function (iq) { // On Error - converse.log("Error occured while trying to fetch the archived messages count", "error"); }.bind(this) - ); - } + }); + return this; }, fetchArchivedMessages: function (options) { From 8d84d5ed03cb7a13a97456fa0b56db18315d94fb Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 20 Jul 2015 19:42:36 +0200 Subject: [PATCH 35/45] Bugfix. Duplicate messages slipped through if they were forwarded. --- converse.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/converse.js b/converse.js index 9625e49e5..322f578e3 100644 --- a/converse.js +++ b/converse.js @@ -3466,8 +3466,7 @@ /* Handler method for all incoming single-user chat "message" stanzas. */ var $message = $(message), - contact_jid, $forwarded, $delay, from_bare_jid, from_resource, is_me, - msgid = $message.attr('id'), + contact_jid, $forwarded, $delay, from_bare_jid, from_resource, is_me, msgid, chatbox, resource, roster_item, from_jid = $message.attr('from'), to_jid = $message.attr('to'), @@ -3493,6 +3492,7 @@ from_bare_jid = Strophe.getBareJidFromJid(from_jid); from_resource = Strophe.getResourceFromJid(from_jid); is_me = from_bare_jid == converse.bare_jid; + msgid = $message.attr('id'); if (is_me) { // I am the sender, so this must be a forwarded message... From 268c4b938984b7327e9a856a3225ffbf00f70b73 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 20 Jul 2015 20:07:29 +0200 Subject: [PATCH 36/45] Add support for receiving messages in random order. Needed for ejabberd support. updates #306 --- converse.js | 71 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/converse.js b/converse.js index 322f578e3..7c396f51a 100644 --- a/converse.js +++ b/converse.js @@ -1380,6 +1380,19 @@ })); }, + appendMessage: function (attrs) { + /* Helper method which appends a message to the end of the chat + * box's content area. + * + * Parameters: + * (Object) attrs: An object containing the message attributes. + */ + _.compose( + _.debounce(this.scrollDown.bind(this), 50), + this.$content.append.bind(this.$content) + )(this.renderMessage(attrs)); + }, + showMessage: function (attrs) { /* Inserts a chat message into the content area of the chat box. * Will also insert a new day indicator if the message is on a @@ -1391,15 +1404,34 @@ * Parameters: * (Object) attrs: An object containing the message attributes. */ - var current_msg_date = moment(attrs.time) || moment, - $first_msg = this.$content.children('.chat-message:first'), + var $first_msg = this.$content.children('.chat-message:first'), first_msg_date = $first_msg.data('isodate'), - last_msg_date = this.$content.children(':last').data('isodate'), - day_date; + last_msg_date, current_msg_date, day_date, $msgs, msg_dates, idx; + if (typeof first_msg_date === "undefined") { + this.appendMessage(attrs); + return; + } + current_msg_date = moment(attrs.time) || moment; + last_msg_date = this.$content.children(':last').data('isodate'); + + if (typeof last_msg_date !== "undefined" && (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date))) { + // The new message is after the last message + if (current_msg_date.isAfter(last_msg_date, 'day')) { + // Append a new day indicator + day_date = moment(current_msg_date).startOf('day'); + this.$content.append(converse.templates.new_day({ + isodate: current_msg_date.format(), + datestring: current_msg_date.format("dddd MMM Do YYYY") + })); + } + this.appendMessage(attrs); + return; + } if (typeof first_msg_date !== "undefined" && - (current_msg_date.isBefore(first_msg_date) || + (current_msg_date.isBefore(first_msg_date) || (current_msg_date.isSame(first_msg_date) && !current_msg_date.isSame(last_msg_date)))) { + // The new message is before the first message if ($first_msg.prev().length === 0) { // There's no day indicator before the first message, so we prepend one. @@ -1427,19 +1459,24 @@ } )(this.renderMessage(attrs)); } - return; - } else if (current_msg_date.isAfter(last_msg_date, 'day')) { - // Append a new day indicator - day_date = moment(current_msg_date).startOf('day'); - this.$content.append(converse.templates.new_day({ - isodate: current_msg_date.format(), - datestring: current_msg_date.format("dddd MMM Do YYYY") - })); + } else { + // We need to find the correct place to position the message + current_msg_date = current_msg_date.format(); + $msgs = this.$content.children('.chat-message'); + msg_dates = _.map($msgs, function (el) { + return $(el).data('isodate'); + }); + msg_dates.push(current_msg_date); + msg_dates.sort(); + idx = msg_dates.indexOf(current_msg_date)-1; + _.compose( + this.scrollDownMessageHeight.bind(this), + function ($el) { + $el.insertAfter(this.$content.find('.chat-message[data-isodate="'+msg_dates[idx]+'"]')); + return $el; + }.bind(this) + )(this.renderMessage(attrs)); } - _.compose( - _.debounce(this.scrollDown.bind(this), 50), - this.$content.append.bind(this.$content) - )(this.renderMessage(attrs)); }, renderMessage: function (attrs) { From e03db127cfbf5dbbaa679818ed6b3813118f0ca2 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 21 Jul 2015 11:35:39 +0200 Subject: [PATCH 37/45] Add a new configuration setting. `muc_history_max_stanzas` --- converse.js | 3 ++- docs/CHANGES.rst | 1 + docs/source/configuration.rst | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/converse.js b/converse.js index 7c396f51a..30f7007cc 100644 --- a/converse.js +++ b/converse.js @@ -324,6 +324,7 @@ keepalive: false, message_archiving: 'never', // Supported values are 'always', 'never', 'roster' (See https://xmpp.org/extensions/xep-0313.html#prefs ) message_carbons: false, // Support for XEP-280 + muc_history_max_stanzas: undefined, // Takes an integer, limits the amount of messages to fetch from chat room's history no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width) ping_interval: 180, //in seconds play_sounds: false, @@ -2774,7 +2775,7 @@ this.occupantsview.chatroomview = this; this.render(); this.occupantsview.model.fetch({add:true}); - this.join(null); + this.join(null, {'maxstanzas': converse.muc_history_max_stanzas}); converse.emit('chatRoomOpened', this); this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index 8fceed668..65415ab0c 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -7,6 +7,7 @@ Changelog * #439 auto_login and keepalive not working [jcbrand] * #440 null added as resource to contact [jcbrand] * Add new event serviceDiscovered [jcbrand] +* Add a new configuration setting `muc_history_max_stanzas`. [jcbrand] 0.9.4 (2015-07-04) ------------------ diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 2fefdcec8..888c793d2 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -361,6 +361,23 @@ Message carbons is the XEP (Jabber protocol extension) specifically drafted to solve this problem, while `forward_messages`_ uses `stanza forwarding `_ +muc_history_max_stanzas +----------------------- + +* Default: ``undefined`` + +This option allows you to specify the maximum amount of messages to be shown in a +chat room when you enter it. By default, the amount specified in the room +configuration or determined by the server will be returned. + +Please note, this option is not related to +`XEP-0313 Message Archive Management `_, +which also allows you to show archived chat room messages, but follows a +different approach. + +If you're using MAM for archiving chat room messages, you might want to set +this option to zero. + expose_rid_and_sid ------------------ From 404ab96609f9238e1e7b3db89ce89801b62ef487 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 21 Jul 2015 11:38:44 +0200 Subject: [PATCH 38/45] Bugfix for groupchat archive queries. Groupchat queries should have a "to" parameter on the IQ stanza and don't use the "with" query option. updates #306 --- converse.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/converse.js b/converse.js index 30f7007cc..012962f07 100644 --- a/converse.js +++ b/converse.js @@ -1320,7 +1320,7 @@ * Then, upon receiving them, call onMessage on the chat box, * so that they are displayed inside it. */ - API.archive.query(options, + API.archive.query(_.extend(options, {'groupchat': this.is_chatroom}), function (messages) { this.clearSpinner(); if (messages.length) { @@ -2779,7 +2779,6 @@ converse.emit('chatRoomOpened', this); this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); - this.model.messages.fetch({add: true}); if (this.model.get('minimized')) { this.hide(); } else { @@ -3037,26 +3036,26 @@ }, join: function (password, history_attrs, extended_presence) { - var msg = $pres({ + var stanza = $pres({ from: converse.connection.jid, to: this.getRoomJIDAndNick() }).c("x", { xmlns: Strophe.NS.MUC }); - if (typeof history_attrs === "object" && history_attrs.length) { - msg = msg.c("history", history_attrs).up(); + if (typeof history_attrs === "object" && Object.keys(history_attrs).length) { + stanza = stanza.c("history", history_attrs).up(); } if (password) { - msg.cnode(Strophe.xmlElement("password", [], password)); + stanza.cnode(Strophe.xmlElement("password", [], password)); } if (typeof extended_presence !== "undefined" && extended_presence !== null) { - msg.up.cnode(extended_presence); + stanza.up.cnode(extended_presence); } if (!this.handler) { this.handler = converse.connection.addHandler(this.handleMUCStanza.bind(this)); } this.model.set('connection_status', Strophe.Status.CONNECTING); - return converse.connection.send(msg); + return converse.connection.send(stanza); }, leave: function(exit_msg) { @@ -3362,6 +3361,7 @@ ($presence.attr('from') == this.model.get('id')+'/'+Strophe.escapeNode(nick)); if (this.model.get('connection_status') !== Strophe.Status.CONNECTED) { this.model.set('connection_status', Strophe.Status.CONNECTED); + this.fetchMessages(); this.$('span.centered.spinner').remove(); this.$el.find('.chat-body').children().show(); } @@ -6334,13 +6334,21 @@ throw new Error('This server does not support XEP-0313, Message Archive Management'); } var queryid = converse.connection.getUniqueId(); - var stanza = $iq({'type':'set'}).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid}); + + var attrs = {'type':'set'}; + if (typeof options != "undefined" && options.groupchat) { + if (!options['with']) { + throw new Error('You need to specify a "with" value containing the chat room JID, when querying groupchat messages.'); + } + attrs.to = options['with']; + } + var stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid}); if (typeof options != "undefined") { stanza.c('x', {'xmlns':Strophe.NS.XFORM}) .c('field', {'var':'FORM_TYPE'}) .c('value').t(Strophe.NS.MAM).up().up(); - if (options['with']) { + if (options['with'] && !options.groupchat) { stanza.c('field', {'var':'with'}).c('value').t(options['with']).up().up(); } _.each(['start', 'end'], function (t) { From 5eea22f2453d92e028e73fb95651e51493020428 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 21 Jul 2015 20:10:16 +0200 Subject: [PATCH 39/45] More work on showing archived chatroom messages. updates #306 --- converse.js | 83 +++++++++++++++++++++++++---------------------------- spec/mam.js | 2 -- 2 files changed, 39 insertions(+), 46 deletions(-) diff --git a/converse.js b/converse.js index 012962f07..e852b6b13 100644 --- a/converse.js +++ b/converse.js @@ -546,39 +546,6 @@ converse.connection.send(pres); }; - this.onMAMQueryResult = function (iq, options, queryid, callback) { - /* Handle the IQ stanza and potential message stanzas returned as - * a result of a MAM (XEP-0313) query. - * - * Parameters: - * (XMLElement) iq - The IQ stanza returned from the XMPP server. - * (Object) options - The MAM-specific options of the query ('with', 'start' and 'end') - * (String) queryid - A unique ID sent with the MAM query. - * (Function) callback - A function to call after we've received all the archived messages. - * If should expect an array of messages and a Strophe.RSM (result set management) object. - */ - var messages = []; - converse.connection.addHandler( - function (message) { - var $msg = $(message), $fin, rsm, i; - if (typeof callback == "function") { - $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); - if ($fin.length) { - rsm = new Strophe.RSM({xml: $fin.find('set')[0]}); - _.extend(rsm, _.pick(options, ['max'])); - _.extend(rsm, _.pick(options, MAM_ATTRIBUTES)); - callback(messages, rsm); - return false; // We've received all messages, decommission this handler - } else if (queryid == $msg.find('result').attr('queryid')) { - messages.push(message); - } - return true; - } else { - return false; // There's no callback, so no use in continuing this handler. - } - }, Strophe.NS.MAM, 'message'); - }; - this.getVCard = function (jid, callback, errback) { /* Request the VCard of another user. * @@ -1324,7 +1291,11 @@ function (messages) { this.clearSpinner(); if (messages.length) { - _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes)); + if (this.is_chatroom) { + _.map(messages, this.onChatRoomMessage.bind(this)); + } else { + _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes)); + } } }.bind(this), _.partial(converse.log, "Error while trying to fetch archived messages", "error") @@ -2776,6 +2747,7 @@ this.render(); this.occupantsview.model.fetch({add:true}); this.join(null, {'maxstanzas': converse.muc_history_max_stanzas}); + this.fetchMessages(); converse.emit('chatRoomOpened', this); this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); @@ -2790,6 +2762,7 @@ this.$el.attr('id', this.model.get('box_id')) .html(converse.templates.chatroom(this.model.toJSON())); this.renderChatArea(); + this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100)); setTimeout(converse.refreshWebkit, 50); return this; }, @@ -3361,7 +3334,6 @@ ($presence.attr('from') == this.model.get('id')+'/'+Strophe.escapeNode(nick)); if (this.model.get('connection_status') !== Strophe.Status.CONNECTED) { this.model.set('connection_status', Strophe.Status.CONNECTED); - this.fetchMessages(); this.$('span.centered.spinner').remove(); this.$el.find('.chat-body').children().show(); } @@ -3372,12 +3344,21 @@ onChatRoomMessage: function (message) { var $message = $(message), - body = $message.children('body').text(), + archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id'), + delayed = $message.find('delay').length > 0, + $forwarded = $message.find('forwarded'), + $delay; + + if ($forwarded.length) { + $message = $forwarded.children('message'); + $delay = $forwarded.children('delay'); + delayed = $delay.length > 0; + } + var body = $message.children('body').text(), jid = $message.attr('from'), msgid = $message.attr('id'), resource = Strophe.getResourceFromJid(jid), sender = resource && Strophe.unescapeNode(resource) || '', - delayed = $message.find('delay').length > 0, subject = $message.children('subject').text(); if (msgid && this.model.messages.findWhere({msgid: msgid})) { @@ -3396,7 +3377,7 @@ if (sender === '') { return true; } - this.model.createMessage($message); + this.model.createMessage($message, $delay, archive_id); if (!delayed && sender !== this.model.get('nick') && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(body)) { converse.playNotification(); } @@ -6325,7 +6306,7 @@ * can be called before passing it in again to this method, to * get the next or previous page in the result set. */ - var date; + var date, messages = []; if (typeof options == "function") { callback = options; errback = callback; @@ -6334,7 +6315,6 @@ throw new Error('This server does not support XEP-0313, Message Archive Management'); } var queryid = converse.connection.getUniqueId(); - var attrs = {'type':'set'}; if (typeof options != "undefined" && options.groupchat) { if (!options['with']) { @@ -6362,16 +6342,31 @@ } }); stanza.up(); - if (options instanceof Strophe.RSM) { stanza.cnode(options.toXML()); } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) { stanza.cnode(new Strophe.RSM(options).toXML()); } } - converse.connection.sendIQ(stanza, - _.partial(converse.onMAMQueryResult, _, options, queryid, callback), - errback); + converse.connection.addHandler(function (message) { + var $msg = $(message), $fin, rsm, i; + if (typeof callback == "function") { + $fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]'); + if ($fin.length) { + rsm = new Strophe.RSM({xml: $fin.find('set')[0]}); + _.extend(rsm, _.pick(options, ['max'])); + _.extend(rsm, _.pick(options, MAM_ATTRIBUTES)); + callback(messages, rsm); + return false; // We've received all messages, decommission this handler + } else if (queryid == $msg.find('result').attr('queryid')) { + messages.push(message); + } + return true; + } else { + return false; // There's no callback, so no use in continuing this handler. + } + }, Strophe.NS.MAM); + converse.connection.sendIQ(stanza, null, errback); } }, 'rooms': { diff --git a/spec/mam.js b/spec/mam.js index 311dfde27..4a4b53ff0 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -316,7 +316,6 @@ sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); - spyOn(converse, 'onMAMQueryResult').andCallThrough(); var callback = jasmine.createSpy('callback'); converse_api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback); @@ -325,7 +324,6 @@ // Send the result stanza, so that the callback is called. var stanza = $iq({'type': 'result', 'id': IQ_id}); converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(converse.onMAMQueryResult).toHaveBeenCalled(); /* * From 44abbeadf395ee28d5d3f157c3248abab6e4ac49 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 21 Jul 2015 20:42:14 +0200 Subject: [PATCH 40/45] Better support for infinite scrolling with MAM for MUC. updates #306 --- converse.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/converse.js b/converse.js index e852b6b13..b02e97ca8 100644 --- a/converse.js +++ b/converse.js @@ -2980,11 +2980,12 @@ handleMUCStanza: function (stanza) { var xmlns, xquery, i; var from = stanza.getAttribute('from'); - if (!from || (this.model.get('id') !== from.split("/")[0])) { + var is_mam = $(stanza).find('[xmlns="'+Strophe.NS.MAM+'"]').length > 0; + if (!from || (this.model.get('id') !== from.split("/")[0]) || is_mam) { return true; } if (stanza.nodeName === "message") { - this.onChatRoomMessage(stanza); + _.compose(this.onChatRoomMessage.bind(this), this.showStatusMessages.bind(this))(stanza); } else if (stanza.nodeName === "presence") { xquery = stanza.getElementsByTagName("x"); if (xquery.length > 0) { @@ -3233,12 +3234,13 @@ 303: ___('Your nickname has been changed to: %1$s') }, - showStatusMessages: function ($el, is_self) { + showStatusMessages: function (el, is_self) { /* Check for status codes and communicate their purpose to the user. * Allow user to configure chat room if they are the owner. * See: http://xmpp.org/registrar/mucstatus.html */ - var disconnect_msgs = [], + var $el = $(el), + disconnect_msgs = [], msgs = [], reasons = []; $el.find('x[xmlns="'+Strophe.NS.MUC_USER+'"]').each(function (idx, x) { @@ -3288,7 +3290,8 @@ for (i=0; i Date: Tue, 21 Jul 2015 21:21:33 +0200 Subject: [PATCH 41/45] MAM-related bugfixes. - Don't show another spinner if one already exists. - last_msg_date's selector was matching chat state notifications. - Remove the spinner when errback gets called. updates #306 --- converse.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/converse.js b/converse.js index b02e97ca8..df345a0e3 100644 --- a/converse.js +++ b/converse.js @@ -1248,7 +1248,9 @@ onScroll: function (ev) { if ($(ev.target).scrollTop() === 0 && this.model.messages.length) { - this.$content.prepend(''); + if (!this.$content.first().hasClass('spinner')) { + this.$content.prepend(''); + } this.fetchArchivedMessages({ 'before': this.model.messages.at(0).get('archive_id'), 'with': this.model.get('jid'), @@ -1298,7 +1300,10 @@ } } }.bind(this), - _.partial(converse.log, "Error while trying to fetch archived messages", "error") + function (iq) { + this.clearSpinner(); + converse.log("Error while trying to fetch archived messages", "error"); + }.bind(this) ); }, @@ -1384,7 +1389,7 @@ return; } current_msg_date = moment(attrs.time) || moment; - last_msg_date = this.$content.children(':last').data('isodate'); + last_msg_date = this.$content.children('.chat-message:last').data('isodate'); if (typeof last_msg_date !== "undefined" && (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date))) { // The new message is after the last message From 1dbe0ed8de6d00af140bcc46df26b1da635af924 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 21 Jul 2015 23:18:03 +0200 Subject: [PATCH 42/45] Trying to work around timezone issues. Causing tests to fail on Travis. --- spec/mam.js | 63 +++++++++++++++-------------------------------------- 1 file changed, 17 insertions(+), 46 deletions(-) diff --git a/spec/mam.js b/spec/mam.js index 4a4b53ff0..8ac6d9315 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -13,6 +13,7 @@ var $iq = converse_api.env.$iq; var $pres = converse_api.env.$pres; var $msg = converse_api.env.$msg; + var moment = converse_api.env.moment; // See: https://xmpp.org/rfcs/rfc3921.html describe("Message Archive Management", $.proxy(function (mock, test_utils) { @@ -74,14 +75,11 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { converse.features.create({'var': Strophe.NS.MAM}); } - // Mock the browser's method for returning the timezone - var getTimezoneOffset = Date.prototype.getTimezoneOffset; - Date.prototype.getTimezoneOffset = function () { - return -120; - }; + var start = '2010-06-07T00:00:00Z'; + var end = '2010-07-07T13:23:54Z'; converse_api.archive.query({ - 'start': '2010-06-07T00:00:00Z', - 'end': '2010-07-07T13:23:54Z' + 'start': start, + 'end': end }); var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); @@ -93,17 +91,15 @@ "urn:xmpp:mam:0"+ ""+ ""+ - "2010-06-07T02:00:00+02:00"+ + ""+moment(start).format()+""+ ""+ ""+ - "2010-07-07T15:23:54+02:00"+ + ""+moment(end).format()+""+ ""+ ""+ ""+ "" ); - // Restore - Date.prototype.getTimezoneOffset = getTimezoneOffset; }); it("throws a TypeError if an invalid date is provided", function () { @@ -122,12 +118,8 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { converse.features.create({'var': Strophe.NS.MAM}); } - // Mock the browser's method for returning the timezone - var getTimezoneOffset = Date.prototype.getTimezoneOffset; - Date.prototype.getTimezoneOffset = function () { - return -120; - }; - converse_api.archive.query({'start': '2010-06-07T00:00:00Z'}); + var start = '2010-06-07T00:00:00Z'; + converse_api.archive.query({'start': start}); var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); expect(sent_stanza.toString()).toBe( ""+ @@ -137,14 +129,12 @@ "urn:xmpp:mam:0"+ ""+ ""+ - "2010-06-07T02:00:00+02:00"+ + ""+moment(start).format()+""+ ""+ ""+ ""+ "" ); - // Restore - Date.prototype.getTimezoneOffset = getTimezoneOffset; }); it("can be used to query for a limited set of results", function () { @@ -157,12 +147,8 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { converse.features.create({'var': Strophe.NS.MAM}); } - // Mock the browser's method for returning the timezone - var getTimezoneOffset = Date.prototype.getTimezoneOffset; - Date.prototype.getTimezoneOffset = function () { - return -120; - }; - converse_api.archive.query({'start': '2010-06-07T00:00:00Z', 'max':10}); + var start = '2010-06-07T00:00:00Z'; + converse_api.archive.query({'start': start, 'max':10}); var queryid = $(sent_stanza.toString()).find('query').attr('queryid'); expect(sent_stanza.toString()).toBe( ""+ @@ -172,7 +158,7 @@ "urn:xmpp:mam:0"+ ""+ ""+ - "2010-06-07T02:00:00+02:00"+ + ""+moment(start).format()+""+ ""+ ""+ ""+ @@ -181,8 +167,6 @@ ""+ "" ); - // Restore - Date.prototype.getTimezoneOffset = getTimezoneOffset; }); it("can be used to page through results", function () { @@ -195,13 +179,9 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { converse.features.create({'var': Strophe.NS.MAM}); } - // Mock the browser's method for returning the timezone - var getTimezoneOffset = Date.prototype.getTimezoneOffset; - Date.prototype.getTimezoneOffset = function () { - return -120; - }; + var start = '2010-06-07T00:00:00Z'; converse_api.archive.query({ - 'start': '2010-06-07T00:00:00Z', + 'start': start, 'after': '09af3-cc343-b409f', 'max':10 }); @@ -214,7 +194,7 @@ "urn:xmpp:mam:0"+ ""+ ""+ - "2010-06-07T02:00:00+02:00"+ + ""+moment(start).format()+""+ ""+ ""+ ""+ @@ -224,8 +204,6 @@ ""+ "" ); - // Restore - Date.prototype.getTimezoneOffset = getTimezoneOffset; }); it("accepts \"before\" with an empty string as value to reverse the order", function () { @@ -271,11 +249,6 @@ sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); - // Mock the browser's method for returning the timezone - var getTimezoneOffset = Date.prototype.getTimezoneOffset; - Date.prototype.getTimezoneOffset = function () { - return -120; - }; var rsm = new Strophe.RSM({'max': '10'}); rsm['with'] = 'romeo@montague.lit'; rsm.start = '2010-06-07T00:00:00Z'; @@ -293,7 +266,7 @@ "romeo@montague.lit"+ ""+ ""+ - "2010-06-07T02:00:00+02:00"+ + ""+moment(rsm.start).format()+""+ ""+ ""+ ""+ @@ -302,8 +275,6 @@ ""+ "" ); - // Restore - Date.prototype.getTimezoneOffset = getTimezoneOffset; }); it("accepts a callback function, which it passes the messages and a Strophe.RSM object", function () { From f0df6df2193b9057b397221eac3ca353870dd836 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 21 Jul 2015 23:24:05 +0200 Subject: [PATCH 43/45] Add a test for serviceDiscovered event. --- spec/disco.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 spec/disco.js diff --git a/spec/disco.js b/spec/disco.js new file mode 100644 index 000000000..c26d997d3 --- /dev/null +++ b/spec/disco.js @@ -0,0 +1,25 @@ +(function (root, factory) { + define([ + "jquery", + "mock", + "test_utils" + ], function ($, mock, test_utils) { + return factory($, mock, test_utils); + } + ); +} (this, function ($, mock, test_utils) { + "use strict"; + var Strophe = converse_api.env.Strophe; + + describe("Service Discovery", $.proxy(function (mock, test_utils) { + + describe("Whenever converse.js discovers a new server feature", $.proxy(function (mock, test_utils) { + it("emits the serviceDiscovered event", function () { + spyOn(converse, 'emit'); + converse.features.create({'var': Strophe.NS.MAM}); + expect(converse.emit).toHaveBeenCalled(); + expect(converse.emit.argsForCall[0][1].get('var')).toBe(Strophe.NS.MAM); + }); + }, converse, mock, test_utils)); + }, converse, mock, test_utils)); +})); From cd4aabc5864073bb9811001ad5223dab9feebc21 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 22 Jul 2015 00:18:30 +0200 Subject: [PATCH 44/45] Rename the page size option and document it. updates #306 --- converse.js | 8 ++++---- docs/source/configuration.rst | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/converse.js b/converse.js index df345a0e3..742cc415d 100644 --- a/converse.js +++ b/converse.js @@ -301,7 +301,7 @@ allow_logout: true, allow_muc: true, allow_otr: true, - archived_messages_batch_size: '20', + archived_messages_page_size: '20', auto_away: 0, // Seconds after which user status is set to 'away' auto_xa: 0, // Seconds after which user status is set to 'xa' allow_registration: true, @@ -1254,7 +1254,7 @@ this.fetchArchivedMessages({ 'before': this.model.messages.at(0).get('archive_id'), 'with': this.model.get('jid'), - 'max': converse.archived_messages_batch_size + 'max': converse.archived_messages_page_size }); } }, @@ -1271,11 +1271,11 @@ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { return; } - if (this.model.messages.length < converse.archived_messages_batch_size) { + if (this.model.messages.length < converse.archived_messages_page_size) { this.fetchArchivedMessages({ 'before': '', // Page backwards from the most recent message 'with': this.model.get('jid'), - 'max': converse.archived_messages_batch_size + 'max': converse.archived_messages_page_size }); } }.bind(this) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 888c793d2..8d7857a42 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -53,6 +53,23 @@ This enables anonymous login if the XMPP server supports it. This option can be used together with `auto_login`_ to automatically and anonymously log a user in as soon as the page loads. +archived_messages_page_size +--------------------------- + +* Default: ``20`` + +See also: `message_archiving` + +This feature applies to `XEP-0313: Message Archive Management (MAM) `_ +and will only take effect if your server supports MAM. + +It allows you to specify the maximum amount of archived messages to be returned per query. +When you open a chat box or room, archived messages will be displayed (if +available) and the amount returned will be no more than the page size. + +You will be able to query for even older messages by scrolling upwards in the chat box or room +(the so-called infinite scrolling pattern). + prebind ~~~~~~~ From 96253f9673269cbdab9c2b6fcf49c22f135f3b4e Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 22 Jul 2015 00:40:51 +0200 Subject: [PATCH 45/45] Document using serviceDiscovery with archive.query. --- docs/source/development.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/source/development.rst b/docs/source/development.rst index 9dbd9c098..21e63368b 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -229,6 +229,7 @@ It accepts the following optional parameters: Examples ^^^^^^^^ + **Requesting all archived messages** The simplest query that can be made is to simply not pass in any parameters. @@ -249,6 +250,29 @@ the returned messages. } converse.archive.query(callback, errback)) +**Waiting until server support has been determined** + +The query method will only work if converse.js has been able to determine that +the server supports MAM queries, otherwise the following error will be raised: + +- *This server does not support XEP-0313, Message Archive Management* + +The very first time converse.js loads in a browser tab, if you call the query +API too quickly, the above error might appear because service discovery has not +yet been completed. + +To work solve this problem, you can first listen for the ``serviceDiscovered`` event, +through which you can be informed once support for MAM has been determined. + +For example: + +.. code-block:: javascript + + converse.listen.on('serviceDiscovered', function (event, feature) { + if (feature.get('var') === converse.env.Strophe.NS.MAM) { + converse.archive.query() + } + }); **Requesting all archived messages for a particular contact or room**