Browse Source

Merge pull request #442 from jcbrand/mam

Message Archive Management
JC Brand 10 years ago
parent
commit
c508b9962c

File diff suppressed because it is too large
+ 413 - 186
converse.js


+ 2 - 0
docs/CHANGES.rst

@@ -6,6 +6,8 @@ Changelog
 
 
 * #439 auto_login and keepalive not working [jcbrand]
 * #439 auto_login and keepalive not working [jcbrand]
 * #440 null added as resource to contact [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)
 0.9.4 (2015-07-04)
 ------------------
 ------------------

+ 47 - 0
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
 used together with `auto_login`_ to automatically and anonymously log a user in
 as soon as the page loads.
 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) <https://xmpp.org/extensions/xep-0313.html>`_
+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
 prebind
 ~~~~~~~
 ~~~~~~~
 
 
@@ -327,6 +344,19 @@ See also:
     `XEP-0198 <http://xmpp.org/extensions/xep-0198.html>`_, specifically
     `XEP-0198 <http://xmpp.org/extensions/xep-0198.html>`_, specifically
     with regards to "stream resumption".
     with regards to "stream resumption".
 
 
+
+message_archiving
+-----------------
+
+* Default:  ``never``
+
+Provides support for `XEP-0313: Message Archive Management <https://xmpp.org/extensions/xep-0313.html>`_
+
+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
 message_carbons
 ---------------
 ---------------
 
 
@@ -348,6 +378,23 @@ Message carbons is the XEP (Jabber protocol extension) specifically drafted to
 solve this problem, while `forward_messages`_ uses
 solve this problem, while `forward_messages`_ uses
 `stanza forwarding <http://www.xmpp.org/extensions/xep-0297.html>`_
 `stanza forwarding <http://www.xmpp.org/extensions/xep-0297.html>`_
 
 
+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 <https://xmpp.org/extensions/xep-0313.html>`_,
+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
 expose_rid_and_sid
 ------------------
 ------------------
 
 

+ 188 - 27
docs/source/development.rst

@@ -51,14 +51,14 @@ directory:
 On Windows you need to specify Makefile.win to be used by running: ::
 On Windows you need to specify Makefile.win to be used by running: ::
 
 
     make -f Makefile.win dev
     make -f Makefile.win dev
-    
+
 Or alternatively, if you don't have GNU Make:
 Or alternatively, if you don't have GNU Make:
 
 
 ::
 ::
 
 
     npm install
     npm install
     bower update
     bower update
-    
+
 This will first install the Node.js development tools (like Grunt and Bower)
 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.
 and then use Bower to install all of Converse.js's front-end dependencies.
 
 
@@ -125,7 +125,7 @@ Please read the `style guide </docs/html/style_guide.html>`_ and make sure that
 Add tests for your bugfix or feature
 Add tests for your bugfix or feature
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Add a test for any bug fixed or feature added. We use Jasmine
 Add a test for any bug fixed or feature added. We use Jasmine
-for testing. 
+for testing.
 
 
 Take a look at `tests.html <https://github.com/jcbrand/converse.js/blob/master/tests.html>`_
 Take a look at `tests.html <https://github.com/jcbrand/converse.js/blob/master/tests.html>`_
 and the `spec files <https://github.com/jcbrand/converse.js/blob/master/tests.html>`_
 and the `spec files <https://github.com/jcbrand/converse.js/blob/master/tests.html>`_
@@ -146,7 +146,7 @@ Developer API
         Earlier versions of Converse.js might have different API methods or none at all.
         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
 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
     .get
     .set
     .set
@@ -202,6 +202,165 @@ Example:
             roster_groups: true
             roster_groups: true
         });
         });
 
 
+The "archive" grouping
+----------------------
+
+Converse.js supports the *Message Archive Management*
+(`XEP-0313 <https://xmpp.org/extensions/xep-0313.html>`_) 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
+~~~~~
+
+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))
+
+**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**
+
+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. Please note, when calling these methods, pass in an integer
+to limit your results.
+
+.. 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(10), 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 an empty string value ``''``.
+
+.. code-block:: javascript
+
+    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:
+        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);
+    }
+
 
 
 The "user" grouping
 The "user" grouping
 -------------------
 -------------------
@@ -213,7 +372,7 @@ logout
 
 
 Log the user out of the current XMPP session.
 Log the user out of the current XMPP session.
 
 
-.. code-block:: javascript 
+.. code-block:: javascript
 
 
     converse.user.logout();
     converse.user.logout();
 
 
@@ -228,7 +387,7 @@ get
 
 
 Return the current user's availability status:
 Return the current user's availability status:
 
 
-.. code-block:: javascript 
+.. code-block:: javascript
 
 
     converse.user.status.get(); // Returns for example "dnd"
     converse.user.status.get(); // Returns for example "dnd"
 
 
@@ -246,7 +405,7 @@ The user's status can be set to one of the following values:
 
 
 For example:
 For example:
 
 
-.. code-block:: javascript 
+.. code-block:: javascript
 
 
     converse.user.status.set('dnd');
     converse.user.status.set('dnd');
 
 
@@ -254,7 +413,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
 message, this method also allows you to pass in a status message as a
 second parameter:
 second parameter:
 
 
-.. code-block:: javascript 
+.. code-block:: javascript
 
 
     converse.user.status.set('dnd', 'In a meeting');
     converse.user.status.set('dnd', 'In a meeting');
 
 
@@ -264,7 +423,7 @@ The "message" sub-grouping
 The ``user.status.message`` sub-grouping exposes methods for setting and
 The ``user.status.message`` sub-grouping exposes methods for setting and
 retrieving the user's custom status message.
 retrieving the user's custom status message.
 
 
-.. code-block:: javascript 
+.. code-block:: javascript
 
 
     converse.user.status.message.set('In a meeting');
     converse.user.status.message.set('In a meeting');
 
 
@@ -344,7 +503,7 @@ Provide the JID of the contact you want to add:
     .. code-block:: javascript
     .. code-block:: javascript
 
 
     converse.contacts.add('buddy@example.com')
     converse.contacts.add('buddy@example.com')
-    
+
 You may also provide the fullname. If not present, we use the jid as fullname:
 You may also provide the fullname. If not present, we use the jid as fullname:
 
 
     .. code-block:: javascript
     .. code-block:: javascript
@@ -580,43 +739,45 @@ Here are the different events that are emitted:
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 | Event Type                      | When is it triggered?                                                                             | Example                                                                                              |
 | Event Type                      | When is it triggered?                                                                             | Example                                                                                              |
 +=================================+===================================================================================================+======================================================================================================+
 +=================================+===================================================================================================+======================================================================================================+
-| **initialized**                 | Once converse.js has been initialized.                                                            | ``converse.listen.on('initialized', function (event) { ... });``                                     |
+| **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) { ... });``            |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **ready**                       | After connection has been established and converse.js has got all its ducks in a row.             | ``converse.listen.on('ready', function (event) { ... });``                                           |
+| **chatBoxOpened**               | When a chat box has been opened.                                                                  | ``converse.listen.on('chatBoxOpened', function (event, chatbox) { ... });``                          |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **reconnect**                   | After the connection has dropped. Converse.js will attempt to reconnect when not in prebind mode. | ``converse.listen.on('reconnect', function (event) { ... });``                                       |
+| **chatRoomOpened**              | When a chat room has been opened.                                                                 | ``converse.listen.on('chatRoomOpened', function (event, chatbox) { ... });``                         |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **message**                     | When a message is received.                                                                       | ``converse.listen.on('message', function (event, messageXML) { ... });``                             |
+| **chatBoxClosed**               | When a chat box has been closed.                                                                  | ``converse.listen.on('chatBoxClosed', function (event, chatbox) { ... });``                          |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **messageSend**                 | When a message will be sent out.                                                                  | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });``          |
+| **chatBoxFocused**              | When the focus has been moved to a chat box.                                                      | ``converse.listen.on('chatBoxFocused', function (event, chatbox) { ... });``                         |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **noResumeableSession**         | When keepalive=true but there aren't any stored prebind tokens.                                   | ``converse.listen.on('noResumeableSession', function (event) { ... });``                             |
+| **chatBoxToggled**              | When a chat box has been minimized or maximized.                                                  | ``converse.listen.on('chatBoxToggled', function (event, chatbox) { ... });``                         |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **roster**                      | When the roster is updated.                                                                       | ``converse.listen.on('roster', function (event, items) { ... });``                                   |
+| **contactStatusChanged**        | When a chat buddy's chat status has changed.                                                      | ``converse.listen.on('contactStatusChanged', function (event, buddy, status) { ... });``             |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **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) { ... });``            |
+| **contactStatusMessageChanged** | When a chat buddy's custom status message has changed.                                            | ``converse.listen.on('contactStatusMessageChanged', function (event, buddy, messageText) { ... });`` |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **chatBoxOpened**               | When a chat box has been opened.                                                                  | ``converse.listen.on('chatBoxOpened', function (event, chatbox) { ... });``                          |
+| **message**                     | When a message is received.                                                                       | ``converse.listen.on('message', function (event, messageXML) { ... });``                             |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **chatRoomOpened**              | When a chat room has been opened.                                                                 | ``converse.listen.on('chatRoomOpened', function (event, chatbox) { ... });``                         |
+| **messageSend**                 | When a message will be sent out.                                                                  | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });``          |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **chatBoxClosed**               | When a chat box has been closed.                                                                  | ``converse.listen.on('chatBoxClosed', function (event, chatbox) { ... });``                          |
+| **noResumeableSession**         | When keepalive=true but there aren't any stored prebind tokens.                                   | ``converse.listen.on('noResumeableSession', function (event) { ... });``                             |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **chatBoxFocused**              | When the focus has been moved to a chat box.                                                      | ``converse.listen.on('chatBoxFocused', function (event, chatbox) { ... });``                         |
+| **initialized**                 | Once converse.js has been initialized.                                                            | ``converse.listen.on('initialized', function (event) { ... });``                                     |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **chatBoxToggled**              | When a chat box has been minimized or maximized.                                                  | ``converse.listen.on('chatBoxToggled', function (event, chatbox) { ... });``                         |
+| **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) { ... });``       |
 | **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) { ... });``       |
 | **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) { ... });``                           |
 | **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) { ... });``                   |
 | **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 (event, service) { ... });``                      |
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 +---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
 
 
 
 
@@ -663,7 +824,7 @@ An example plugin
     }(this, function ($, strophe, utils, converse_api) {
     }(this, function ($, strophe, utils, converse_api) {
 
 
         // Wrap your UI strings with the __ function for translation support.
         // Wrap your UI strings with the __ function for translation support.
-        var __ = $.proxy(utils.__, this); 
+        var __ = $.proxy(utils.__, this);
 
 
         // Strophe methods for building stanzas
         // Strophe methods for building stanzas
         var Strophe = strophe.Strophe;
         var Strophe = strophe.Strophe;

+ 7 - 7
main.js

@@ -26,18 +26,19 @@ require.config({
         "jquery-private":           "src/jquery-private",
         "jquery-private":           "src/jquery-private",
         "jquery.browser":           "components/jquery.browser/dist/jquery.browser",
         "jquery.browser":           "components/jquery.browser/dist/jquery.browser",
         "jquery.easing":            "components/jquery-easing-original/index",          // XXX: Only required for https://conversejs.org website
         "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":                  "components/strophejs/src/wrapper",
         "strophe-base64":           "components/strophejs/src/base64",
         "strophe-base64":           "components/strophejs/src/base64",
         "strophe-bosh":             "components/strophejs/src/bosh",
         "strophe-bosh":             "components/strophejs/src/bosh",
         "strophe-core":             "components/strophejs/src/core",
         "strophe-core":             "components/strophejs/src/core",
-        "strophe":                  "components/strophejs/src/wrapper",
         "strophe-md5":              "components/strophejs/src/md5",
         "strophe-md5":              "components/strophejs/src/md5",
+        "strophe-polyfill":         "components/strophejs/src/polyfills",
         "strophe-sha1":             "components/strophejs/src/sha1",
         "strophe-sha1":             "components/strophejs/src/sha1",
         "strophe-websocket":        "components/strophejs/src/websocket",
         "strophe-websocket":        "components/strophejs/src/websocket",
-        "strophe-polyfill":         "components/strophejs/src/polyfills",
         "strophe.disco":            "components/strophejs-plugins/disco/strophe.disco",
         "strophe.disco":            "components/strophejs-plugins/disco/strophe.disco",
-        "strophe.vcard":            "src/strophe.vcard",
         "strophe.ping":             "src/strophe.ping",
         "strophe.ping":             "src/strophe.ping",
+        "strophe.rsm":              "components/strophejs-plugins/rsm/strophe.rsm",
+        "strophe.vcard":            "src/strophe.vcard",
         "text":                     'components/requirejs-text/text',
         "text":                     'components/requirejs-text/text',
         "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
         "tpl":                      'components/requirejs-tpl-jcbrand/tpl',
         "typeahead":                "components/typeahead.js/index",
         "typeahead":                "components/typeahead.js/index",
@@ -185,10 +186,9 @@ require.config({
         'crypto.sha1':          { deps: ['crypto.core'] },
         'crypto.sha1':          { deps: ['crypto.core'] },
         'crypto.sha256':        { deps: ['crypto.core'] },
         'crypto.sha256':        { deps: ['crypto.core'] },
         'bigint':               { deps: ['crypto'] },
         'bigint':               { deps: ['crypto'] },
-        'strophe.disco':        { deps: ['strophe'] },
+        'strophe.ping':         { deps: ['strophe'] },
         'strophe.register':     { deps: ['strophe'] },
         'strophe.register':     { deps: ['strophe'] },
-        'strophe.vcard':        { deps: ['strophe'] },
-        'strophe.ping':         { deps: ['strophe'] }
+        'strophe.vcard':        { deps: ['strophe'] }
     }
     }
 });
 });
 
 

+ 1 - 1
spec/chatbox.js

@@ -652,7 +652,7 @@
                     var message_date = new Date();
                     var message_date = new Date();
                     expect($time.length).toEqual(1);
                     expect($time.length).toEqual(1);
                     expect($time.attr('class')).toEqual('chat-date');
                     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"));
                     expect($time.text()).toEqual(moment(message_date).format("dddd MMM Do YYYY"));
 
 
                     // Normal checks for the 2nd message
                     // Normal checks for the 2nd message

+ 4 - 0
spec/converse.js

@@ -299,6 +299,10 @@
                 var box = converse_api.chats.open(jid);
                 var box = converse_api.chats.open(jid);
                 expect(box instanceof Object).toBeTruthy();
                 expect(box instanceof Object).toBeTruthy();
                 expect(box.get('box_id')).toBe(b64_sha1(jid));
                 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);
                 var chatboxview = this.chatboxviews.get(jid);
                 expect(chatboxview.$el.is(':visible')).toBeTruthy();
                 expect(chatboxview.$el.is(':visible')).toBeTruthy();
 
 

+ 25 - 0
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));
+}));

+ 448 - 0
spec/mam.js

@@ -0,0 +1,448 @@
+(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;
+    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) {
+        // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
+
+        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(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'><query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'/></iq>");
+            });
+
+           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({'with':'juliet@capulet.lit'});
+                var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
+                expect(sent_stanza.toString()).toBe(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
+                            "<x xmlns='jabber:x:data'>"+
+                            "<field var='FORM_TYPE'>"+
+                                "<value>urn:xmpp:mam:0</value>"+
+                            "</field>"+
+                            "<field var='with'>"+
+                                "<value>juliet@capulet.lit</value>"+
+                            "</field>"+
+                            "</x>"+
+                        "</query>"+
+                    "</iq>"
+                );
+            });
+
+           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});
+                }
+                var start = '2010-06-07T00:00:00Z';
+                var end = '2010-07-07T13:23:54Z';
+                converse_api.archive.query({
+                    'start': start,
+                    'end': end
+
+                });
+                var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
+                expect(sent_stanza.toString()).toBe(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
+                            "<x xmlns='jabber:x:data'>"+
+                            "<field var='FORM_TYPE'>"+
+                                "<value>urn:xmpp:mam:0</value>"+
+                            "</field>"+
+                            "<field var='start'>"+
+                                "<value>"+moment(start).format()+"</value>"+
+                            "</field>"+
+                            "<field var='end'>"+
+                                "<value>"+moment(end).format()+"</value>"+
+                            "</field>"+
+                            "</x>"+
+                        "</query>"+
+                    "</iq>"
+                );
+           });
+
+           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});
+                }
+                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(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
+                            "<x xmlns='jabber:x:data'>"+
+                            "<field var='FORM_TYPE'>"+
+                                "<value>urn:xmpp:mam:0</value>"+
+                            "</field>"+
+                            "<field var='start'>"+
+                                "<value>"+moment(start).format()+"</value>"+
+                            "</field>"+
+                            "</x>"+
+                        "</query>"+
+                    "</iq>"
+                );
+           });
+
+           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});
+                }
+                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(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
+                            "<x xmlns='jabber:x:data'>"+
+                                "<field var='FORM_TYPE'>"+
+                                    "<value>urn:xmpp:mam:0</value>"+
+                                "</field>"+
+                                "<field var='start'>"+
+                                    "<value>"+moment(start).format()+"</value>"+
+                                "</field>"+
+                            "</x>"+
+                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                "<max>10</max>"+
+                            "</set>"+
+                        "</query>"+
+                    "</iq>"
+                );
+           });
+
+           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});
+                }
+                var start = '2010-06-07T00:00:00Z';
+                converse_api.archive.query({
+                    'start': start,
+                    'after': '09af3-cc343-b409f',
+                    'max':10
+                });
+                var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
+                expect(sent_stanza.toString()).toBe(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
+                            "<x xmlns='jabber:x:data'>"+
+                                "<field var='FORM_TYPE'>"+
+                                    "<value>urn:xmpp:mam:0</value>"+
+                                "</field>"+
+                                "<field var='start'>"+
+                                    "<value>"+moment(start).format()+"</value>"+
+                                "</field>"+
+                            "</x>"+
+                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                "<max>10</max>"+
+                                "<after>09af3-cc343-b409f</after>"+
+                            "</set>"+
+                        "</query>"+
+                    "</iq>"
+                );
+           });
+
+           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(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
+                            "<x xmlns='jabber:x:data'>"+
+                                "<field var='FORM_TYPE'>"+
+                                    "<value>urn:xmpp:mam:0</value>"+
+                                "</field>"+
+                            "</x>"+
+                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                "<max>10</max>"+
+                                "<before></before>"+
+                            "</set>"+
+                        "</query>"+
+                    "</iq>"
+                );
+           });
+
+           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.
+                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);
+                });
+                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(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
+                            "<x xmlns='jabber:x:data'>"+
+                                "<field var='FORM_TYPE'>"+
+                                    "<value>urn:xmpp:mam:0</value>"+
+                                "</field>"+
+                                "<field var='with'>"+
+                                    "<value>romeo@montague.lit</value>"+
+                                "</field>"+
+                                "<field var='start'>"+
+                                    "<value>"+moment(rsm.start).format()+"</value>"+
+                                "</field>"+
+                            "</x>"+
+                            "<set xmlns='http://jabber.org/protocol/rsm'>"+
+                                "<max>10</max>"+
+                            "</set>"+
+                        "</query>"+
+                    "</iq>"
+                );
+           });
+
+           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);
+                });
+                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));
+
+                /* <message id='aeb213' to='juliet@capulet.lit/chamber'>
+                 *   <result xmlns='urn:xmpp:mam:0' queryid='f27' id='28482-98726-73623'>
+                 *     <forwarded xmlns='urn:xmpp:forward:0'>
+                 *       <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
+                 *       <message
+                 *         to='juliet@capulet.lit/balcony'
+                 *         from='romeo@montague.lit/orchard'
+                 *         type='chat'
+                 *         xmlns='jabber:client'>
+                 *         <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
+                 *       </message>
+                 *     </forwarded>
+                 *   </result>
+                 * </message>
+                 */
+                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 <fin> message to indicate the end of the result set.
+                 *
+                 * <message>
+                 *     <fin xmlns='urn:xmpp:mam:0' complete='true'>
+                 *         <set xmlns='http://jabber.org/protocol/rsm'>
+                 *             <first index='0'>23452-4534-1</first>
+                 *             <last>390-2342-22</last>
+                 *             <count>16</count>
+                 *         </set>
+                 *     </fin>
+                 * </message>
+                 */
+                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[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');
+                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) {
+
+            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(
+                    "<iq type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<prefs xmlns='urn:xmpp:mam:0'/>"+
+                    "</iq>"
+                );
+
+                converse.message_archiving = 'never';
+                /* Example 15. Server responds with current preferences
+                 *
+                 * <iq type='result' id='juliet2'>
+                 *   <prefs xmlns='urn:xmpp:mam:0' default='roster'>
+                 *     <always/>
+                 *     <never/>
+                 *   </prefs>
+                 * </iq>
+                 */
+                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(
+                    "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                        "<prefs xmlns='urn:xmpp:mam:0' default='never'>"+
+                            "<always><jid>romeo@montague.lit</jid></always>"+
+                            "<never><jid>montague@montague.lit</jid></never>"+
+                        "</prefs>"+
+                    "</iq>"
+                );
+
+                expect(feature.get('preference')).toBe(undefined);
+                /* <iq type='result' id='juliet3'>
+                 *   <prefs xmlns='urn:xmpp:mam:0' default='always'>
+                 *       <always>
+                 *          <jid>romeo@montague.lit</jid>
+                 *       </always>
+                 *       <never>
+                 *          <jid>montague@montague.lit</jid>
+                 *       </never>
+                 *   </prefs>
+                 * </iq>
+                 */
+                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));
+}));

+ 2 - 1
src/deps-full.js

@@ -4,9 +4,10 @@ define("converse-dependencies", [
     "otr",
     "otr",
     "moment_with_locales",
     "moment_with_locales",
     "strophe",
     "strophe",
-    "strophe.vcard",
     "strophe.disco",
     "strophe.disco",
     "strophe.ping",
     "strophe.ping",
+    "strophe.rsm",
+    "strophe.vcard",
     "backbone.browserStorage",
     "backbone.browserStorage",
     "backbone.overview",
     "backbone.overview",
     "jquery.browser",
     "jquery.browser",

+ 2 - 1
src/deps-no-otr.js

@@ -3,9 +3,10 @@ define("converse-dependencies", [
     "utils",
     "utils",
     "moment_with_locales",
     "moment_with_locales",
     "strophe",
     "strophe",
-    "strophe.vcard",
     "strophe.disco",
     "strophe.disco",
     "strophe.ping",
     "strophe.ping",
+    "strophe.rsm",
+    "strophe.vcard",
     "backbone.browserStorage",
     "backbone.browserStorage",
     "backbone.overview",
     "backbone.overview",
     "jquery.browser",
     "jquery.browser",

+ 2 - 1
src/deps-website-no-otr.js

@@ -3,9 +3,10 @@ define("converse-dependencies", [
     "utils",
     "utils",
     "moment_with_locales",
     "moment_with_locales",
     "strophe",
     "strophe",
-    "strophe.vcard",
     "strophe.disco",
     "strophe.disco",
     "strophe.ping",
     "strophe.ping",
+    "strophe.rsm",
+    "strophe.vcard",
     "bootstrapJS", // XXX: Can be removed, only for https://conversejs.org
     "bootstrapJS", // XXX: Can be removed, only for https://conversejs.org
     "backbone.browserStorage",
     "backbone.browserStorage",
     "backbone.overview",
     "backbone.overview",

+ 2 - 1
src/deps-website.js

@@ -5,9 +5,10 @@ define("converse-dependencies", [
     "otr",
     "otr",
     "moment_with_locales",
     "moment_with_locales",
     "strophe",
     "strophe",
-    "strophe.vcard",
     "strophe.disco",
     "strophe.disco",
     "strophe.ping",
     "strophe.ping",
+    "strophe.rsm",
+    "strophe.vcard",
     "bootstrapJS", // XXX: Only for https://conversejs.org
     "bootstrapJS", // XXX: Only for https://conversejs.org
     "backbone.browserStorage",
     "backbone.browserStorage",
     "backbone.overview",
     "backbone.overview",

+ 1 - 1
src/templates/action.html

@@ -1,4 +1,4 @@
-<div class="chat-message {{extra_classes}}">
+<div class="chat-message {{extra_classes}}" data-isodate="{{isodate}}">
     <span class="chat-message-{{sender}}">{{time}} **{{username}} </span>
     <span class="chat-message-{{sender}}">{{time}} **{{username}} </span>
     <span class="chat-message-content">{{message}}</span>
     <span class="chat-message-content">{{message}}</span>
 </div>
 </div>

+ 1 - 1
src/templates/message.html

@@ -1,4 +1,4 @@
-<div class="chat-message {{extra_classes}}">
+<div class="chat-message {{extra_classes}}" data-isodate="{{isodate}}">
     <span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>
     <span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>
     <span class="chat-message-content">{{message}}</span>
     <span class="chat-message-content">{{message}}</span>
 </div>
 </div>

+ 1 - 1
src/templates/new_day.html

@@ -1 +1 @@
-<time class="chat-date" datetime="{{isodate}}">{{datestring}}</time>
+<time class="chat-date" data-isodate="{{isodate}}">{{datestring}}</time>

+ 35 - 0
src/utils.js

@@ -50,6 +50,41 @@
         return this;
         return this;
     };
     };
 
 
+    $.fn.addEmoticons = function (allowed) {
+        if (allowed) {
+            if (this.length > 0) {
+                this.each(function (i, obj) {
+                    var text = $(obj).html();
+                    text = text.replace(/&gt;:\)/g, '<span class="emoticon icon-evil"></span>');
+                    text = text.replace(/:\)/g, '<span class="emoticon icon-smiley"></span>');
+                    text = text.replace(/:\-\)/g, '<span class="emoticon icon-smiley"></span>');
+                    text = text.replace(/;\)/g, '<span class="emoticon icon-wink"></span>');
+                    text = text.replace(/;\-\)/g, '<span class="emoticon icon-wink"></span>');
+                    text = text.replace(/:D/g, '<span class="emoticon icon-grin"></span>');
+                    text = text.replace(/:\-D/g, '<span class="emoticon icon-grin"></span>');
+                    text = text.replace(/:P/g, '<span class="emoticon icon-tongue"></span>');
+                    text = text.replace(/:\-P/g, '<span class="emoticon icon-tongue"></span>');
+                    text = text.replace(/:p/g, '<span class="emoticon icon-tongue"></span>');
+                    text = text.replace(/:\-p/g, '<span class="emoticon icon-tongue"></span>');
+                    text = text.replace(/8\)/g, '<span class="emoticon icon-cool"></span>');
+                    text = text.replace(/:S/g, '<span class="emoticon icon-confused"></span>');
+                    text = text.replace(/:\\/g, '<span class="emoticon icon-wondering"></span>');
+                    text = text.replace(/:\/ /g, '<span class="emoticon icon-wondering"></span>');
+                    text = text.replace(/&gt;:\(/g, '<span class="emoticon icon-angry"></span>');
+                    text = text.replace(/:\(/g, '<span class="emoticon icon-sad"></span>');
+                    text = text.replace(/:\-\(/g, '<span class="emoticon icon-sad"></span>');
+                    text = text.replace(/:O/g, '<span class="emoticon icon-shocked"></span>');
+                    text = text.replace(/:\-O/g, '<span class="emoticon icon-shocked"></span>');
+                    text = text.replace(/\=\-O/g, '<span class="emoticon icon-shocked"></span>');
+                    text = text.replace(/\(\^.\^\)b/g, '<span class="emoticon icon-thumbs-up"></span>');
+                    text = text.replace(/&lt;3/g, '<span class="emoticon icon-heart"></span>');
+                    $(obj).html(text);
+                });
+            }
+        }
+        return this;
+    };
+
     var utils = {
     var utils = {
         // Translation machinery
         // Translation machinery
         // ---------------------
         // ---------------------

+ 2 - 0
tests/main.js

@@ -60,7 +60,9 @@ require([
             require([
             require([
                 "console-runner",
                 "console-runner",
                 "spec/converse",
                 "spec/converse",
+                "spec/disco",
                 "spec/protocol",
                 "spec/protocol",
+                "spec/mam",
                 "spec/otr",
                 "spec/otr",
                 "spec/eventemitter",
                 "spec/eventemitter",
                 "spec/controlbox",
                 "spec/controlbox",

Some files were not shown because too many files changed in this diff