Browse Source

Add support for paging through MAM results when catching up

Fixes #1548
JC Brand 6 years ago
parent
commit
8a9a0a4b19
3 changed files with 213 additions and 9 deletions
  1. 1 0
      CHANGES.md
  2. 190 1
      spec/mam.js
  3. 22 8
      src/headless/converse-mam.js

+ 1 - 0
CHANGES.md

@@ -47,6 +47,7 @@
 - #1524: OMEMO libsignal-protocol.js Invalid signature
 - #1524: OMEMO libsignal-protocol.js Invalid signature
 - #1532: Converse reloads on enter pressed in the filter box
 - #1532: Converse reloads on enter pressed in the filter box
 - #1538: Allow adding self as contact
 - #1538: Allow adding self as contact
+- #1548: Add support for paging through the MAM results when filling in the blanks
 - #1550: Legitimate carbons being blocked due to erroneous forgery check
 - #1550: Legitimate carbons being blocked due to erroneous forgery check
 - #1554: Room auto-configuration broke if the config form contained fields with type `fixed`
 - #1554: Room auto-configuration broke if the config form contained fields with type `fixed`
 - #1558: `this.get` is not a function error when `forward_messages` is set to `true`.
 - #1558: `this.get` is not a function error when `forward_messages` is set to `true`.

+ 190 - 1
spec/mam.js

@@ -12,8 +12,197 @@
     const sizzle = converse.env.sizzle;
     const sizzle = converse.env.sizzle;
     // See: https://xmpp.org/rfcs/rfc3921.html
     // See: https://xmpp.org/rfcs/rfc3921.html
 
 
+    // Implements the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
     describe("Message Archive Management", function () {
     describe("Message Archive Management", function () {
-        // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
+
+        describe("The XEP-0313 Archive", function () {
+
+            it("is queried when the user enters a new MUC",
+                mock.initConverse(
+                    null, ['discoInitialized'], {'archived_messages_page_size': 2},
+                    async function (done, _converse) {
+
+                spyOn(_converse.ChatBox.prototype, 'fetchArchivedMessages').and.callThrough();
+                const sent_IQs = _converse.connection.IQ_stanzas;
+                const muc_jid = 'orchard@chat.shakespeare.lit';
+                await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+                let view = _converse.chatboxviews.get(muc_jid);
+                let iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
+                expect(Strophe.serialize(iq_get)).toBe(
+                    `<iq id="${iq_get.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
+                        `<query queryid="${iq_get.querySelector('query').getAttribute('queryid')}" xmlns="${Strophe.NS.MAM}">`+
+                            `<x type="submit" xmlns="jabber:x:data">`+
+                                `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
+                            `</x>`+
+                            `<set xmlns="http://jabber.org/protocol/rsm"><max>2</max><before></before></set>`+
+                        `</query>`+
+                    `</iq>`);
+
+                let first_msg_id = _converse.connection.getUniqueId();
+                let last_msg_id = _converse.connection.getUniqueId();
+                let message = u.toStanza(
+                    `<message xmlns="jabber:client"
+                            to="romeo@montague.lit/orchard"
+                            from="${muc_jid}">
+                        <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${first_msg_id}">
+                            <forwarded xmlns="urn:xmpp:forward:0">
+                                <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
+                                <message from="${muc_jid}/some1" type="groupchat">
+                                    <body>2nd Message</body>
+                                </message>
+                            </forwarded>
+                        </result>
+                    </message>`);
+                _converse.connection._dataRecv(test_utils.createRequest(message));
+
+                message = u.toStanza(
+                    `<message xmlns="jabber:client"
+                            to="romeo@montague.lit/orchard"
+                            from="${muc_jid}">
+                        <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${last_msg_id}">
+                            <forwarded xmlns="urn:xmpp:forward:0">
+                                <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
+                                <message from="${muc_jid}/some1" type="groupchat">
+                                    <body>3rd Message</body>
+                                </message>
+                            </forwarded>
+                        </result>
+                    </message>`);
+                _converse.connection._dataRecv(test_utils.createRequest(message));
+
+                // Clear so that we don't match the older query
+                while (sent_IQs.length) { sent_IQs.pop(); }
+
+                // XXX: Even though the count is 3, when fetching messages for
+                // the first time, we don't paginate, so that message
+                // is not fetched. The user needs to manually load older
+                // messages for it to be fetched.
+                // TODO: we need to add a clickable link to load older messages
+                let result = u.toStanza(
+                    `<iq type='result' id='${iq_get.getAttribute('id')}'>
+                        <fin xmlns='urn:xmpp:mam:2'>
+                            <set xmlns='http://jabber.org/protocol/rsm'>
+                                <first index='0'>${first_msg_id}</first>
+                                <last>${last_msg_id}</last>
+                                <count>3</count>
+                            </set>
+                        </fin>
+                    </iq>`);
+                _converse.connection._dataRecv(test_utils.createRequest(result));
+                await u.waitUntil(() => view.model.messages.length === 2);
+                view.close();
+                // Clear so that we don't match the older query
+                while (sent_IQs.length) { sent_IQs.pop(); }
+
+                await u.waitUntil(() => _converse.chatboxes.length === 1);
+                await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
+                view = _converse.chatboxviews.get(muc_jid);
+                await u.waitUntil(() => view.model.messages.length);
+
+                iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
+                expect(Strophe.serialize(iq_get)).toBe(
+                    `<iq id="${iq_get.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
+                        `<query queryid="${iq_get.querySelector('query').getAttribute('queryid')}" xmlns="${Strophe.NS.MAM}">`+
+                            `<x type="submit" xmlns="jabber:x:data">`+
+                                `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
+                            `</x>`+
+                            `<set xmlns="http://jabber.org/protocol/rsm"><max>2</max><after>${message.querySelector('result').getAttribute('id')}</after><before></before></set>`+
+                        `</query>`+
+                    `</iq>`);
+
+                first_msg_id = _converse.connection.getUniqueId();
+                last_msg_id = _converse.connection.getUniqueId();
+                message = u.toStanza(
+                    `<message xmlns="jabber:client"
+                            to="romeo@montague.lit/orchard"
+                            from="${muc_jid}">
+                        <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${first_msg_id}">
+                            <forwarded xmlns="urn:xmpp:forward:0">
+                                <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
+                                <message from="${muc_jid}/some1" type="groupchat">
+                                    <body>4th Message</body>
+                                </message>
+                            </forwarded>
+                        </result>
+                    </message>`);
+                _converse.connection._dataRecv(test_utils.createRequest(message));
+
+                message = u.toStanza(
+                    `<message xmlns="jabber:client"
+                            to="romeo@montague.lit/orchard"
+                            from="${muc_jid}">
+                        <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${last_msg_id}">
+                            <forwarded xmlns="urn:xmpp:forward:0">
+                                <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
+                                <message from="${muc_jid}/some1" type="groupchat">
+                                    <body>5th Message</body>
+                                </message>
+                            </forwarded>
+                        </result>
+                    </message>`);
+                _converse.connection._dataRecv(test_utils.createRequest(message));
+
+                // Clear so that we don't match the older query
+                while (sent_IQs.length) { sent_IQs.pop(); }
+
+                result = u.toStanza(
+                    `<iq type='result' id='${iq_get.getAttribute('id')}'>
+                        <fin xmlns='urn:xmpp:mam:2'>
+                            <set xmlns='http://jabber.org/protocol/rsm'>
+                                <first index='0'>${first_msg_id}</first>
+                                <last>${last_msg_id}</last>
+                                <count>5</count>
+                            </set>
+                        </fin>
+                    </iq>`);
+                _converse.connection._dataRecv(test_utils.createRequest(result));
+                await u.waitUntil(() => view.model.messages.length === 4);
+
+                iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
+                expect(Strophe.serialize(iq_get)).toBe(
+                    `<iq id="${iq_get.getAttribute('id')}" to="orchard@chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
+                        `<query queryid="${iq_get.querySelector('query').getAttribute('queryid')}" xmlns="urn:xmpp:mam:2">`+
+                            `<x type="submit" xmlns="jabber:x:data">`+
+                                `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
+                            `</x>`+
+                            `<set xmlns="http://jabber.org/protocol/rsm">`+
+                                `<max>2</max><before>${first_msg_id}</before>`+
+                            `</set>`+
+                        `</query>`+
+                    `</iq>`);
+
+                const msg_id = _converse.connection.getUniqueId();
+                message = u.toStanza(
+                    `<message xmlns="jabber:client"
+                            to="romeo@montague.lit/orchard"
+                            from="${muc_jid}">
+                        <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${msg_id}">
+                            <forwarded xmlns="urn:xmpp:forward:0">
+                                <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
+                                <message from="${muc_jid}/some1" type="groupchat">
+                                    <body>6th Message</body>
+                                </message>
+                            </forwarded>
+                        </result>
+                    </message>`);
+                _converse.connection._dataRecv(test_utils.createRequest(message));
+
+                result = u.toStanza(
+                    `<iq type='result' id='${iq_get.getAttribute('id')}'>
+                        <fin xmlns="urn:xmpp:mam:2" complete="true">
+                            <set xmlns="http://jabber.org/protocol/rsm">
+                                <first index="0">${msg_id}</first>
+                                <last>${msg_id}</last>
+                                <count>6</count>
+                            </set>
+                        </fin>
+                    </iq>`);
+                _converse.connection._dataRecv(test_utils.createRequest(result));
+                await u.waitUntil(() => view.model.messages.length === 5);
+                expect(view.model.fetchArchivedMessages.calls.count()).toBe(3);
+                done();
+            }));
+        });
 
 
         describe("An archived message", function () {
         describe("An archived message", function () {
 
 

+ 22 - 8
src/headless/converse-mam.js

@@ -102,14 +102,25 @@ converse.plugins.add('converse-mam', {
                 } else {
                 } else {
                     message_handler = _converse.chatboxes.onMessage.bind(_converse.chatboxes)
                     message_handler = _converse.chatboxes.onMessage.bind(_converse.chatboxes)
                 }
                 }
-                const result = await _converse.api.archive.query(
-                    Object.assign({
+                const query = Object.assign({
                         'groupchat': is_groupchat,
                         'groupchat': is_groupchat,
                         'before': '', // Page backwards from the most recent message
                         'before': '', // Page backwards from the most recent message
                         'max': _converse.archived_messages_page_size,
                         'max': _converse.archived_messages_page_size,
                         'with': this.get('jid'),
                         'with': this.get('jid'),
-                    }, options));
+                    }, options);
+                const result = await _converse.api.archive.query(query);
                 result.messages.forEach(message_handler);
                 result.messages.forEach(message_handler);
+
+                const catching_up = query.before || query.after;
+                if (result.rsm) {
+                    if (catching_up) {
+                        return this.fetchArchivedMessages(result.rsm.previous(_converse.archived_messages_page_size));
+                    } else {
+                        // TODO: Add a special kind of message which will
+                        // render as a link to fetch further messages, either
+                        // to fetch older messages or to fill in a gap.
+                    }
+                }
             },
             },
 
 
             async findDuplicateFromArchiveID (stanza) {
             async findDuplicateFromArchiveID (stanza) {
@@ -455,7 +466,7 @@ converse.plugins.add('converse-mam', {
                         stanza.up();
                         stanza.up();
                         if (options instanceof _converse.RSM) {
                         if (options instanceof _converse.RSM) {
                             stanza.cnode(options.toXML());
                             stanza.cnode(options.toXML());
-                        } else if (_.intersection(_converse.RSM_ATTRIBUTES, Object.keys(options)).length) {
+                        } else if (intersection(_converse.RSM_ATTRIBUTES, Object.keys(options)).length) {
                             stanza.cnode(new _converse.RSM(options).toXML());
                             stanza.cnode(new _converse.RSM(options).toXML());
                         }
                         }
                     }
                     }
@@ -483,10 +494,13 @@ converse.plugins.add('converse-mam', {
                     }
                     }
                     _converse.connection.deleteHandler(message_handler);
                     _converse.connection.deleteHandler(message_handler);
 
 
-                    const set = iq_result ? iq_result.querySelector('set') : null;
-                    if (set !== null) {
-                        rsm = new _converse.RSM({'xml': set});
-                        Object.assign(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max'])));
+                    const fin = iq_result && sizzle(`fin[xmlns="${Strophe.NS.MAM}"]`, iq_result).pop();
+                    if (fin && [null, 'false'].includes(fin.getAttribute('complete'))) {
+                        const set = sizzle(`set[xmlns="${Strophe.NS.RSM}"]`, fin).pop();
+                        if (set) {
+                            rsm = new _converse.RSM({'xml': set});
+                            Object.assign(rsm, pick(options, [...MAM_ATTRIBUTES, ..._converse.RSM_ATTRIBUTES]));
+                        }
                     }
                     }
                     return { messages, rsm }
                     return { messages, rsm }
                 }
                 }