Browse Source

Merge remote-tracking branch 'origin/master'

Weblate 7 years ago
parent
commit
26d3455fa5
4 changed files with 178 additions and 149 deletions
  1. 1 0
      CHANGES.md
  2. 35 43
      src/converse-chatview.js
  3. 98 94
      src/converse-mam.js
  4. 44 12
      src/utils.js

+ 1 - 0
CHANGES.md

@@ -47,6 +47,7 @@
 - `hide_open_bookmarks` is now by default `true`.
 - `hide_open_bookmarks` is now by default `true`.
 
 
 ### UX/UI changes
 ### UX/UI changes
+- #984 Improve loading of archived messages via "infinite scroll"
 - Use CSS3 fade transitions to render various elements.
 - Use CSS3 fade transitions to render various elements.
 - Remove `Login` and `Registration` tabs and consolidate into one panel.
 - Remove `Login` and `Registration` tabs and consolidate into one panel.
 - Show validation error messages on the login form.
 - Show validation error messages on the login form.

+ 35 - 43
src/converse-chatview.js

@@ -404,7 +404,7 @@
                      *      This element must have a "data-isodate" attribute
                      *      This element must have a "data-isodate" attribute
                      *      which specifies its creation date.
                      *      which specifies its creation date.
                      */
                      */
-                    const prev_msg_el = this.getPreviousMessageElement(next_msg_el),
+                    const prev_msg_el = u.getPreviousElement(next_msg_el, ".message:not(.chat-event)"),
                           prev_msg_date = _.isNull(prev_msg_el) ? null : prev_msg_el.getAttribute('data-isodate'),
                           prev_msg_date = _.isNull(prev_msg_el) ? null : prev_msg_el.getAttribute('data-isodate'),
                           next_msg_date = next_msg_el.getAttribute('data-isodate');
                           next_msg_date = next_msg_el.getAttribute('data-isodate');
 
 
@@ -419,34 +419,6 @@
                     }
                     }
                 },
                 },
 
 
-                isNotPermanentMessage (el) {
-                    return !_.isNull(el) && (u.hasClass('chat-event', el) || !u.hasClass('message', el));
-                },
-
-                getPreviousMessageElement (el) {
-                    let prev_msg_el = el.previousSibling;
-                    while (this.isNotPermanentMessage(prev_msg_el)) {
-                        prev_msg_el = prev_msg_el.previousSibling
-                    }
-                    return prev_msg_el;
-                },
-
-                getLastMessageElement () {
-                    let last_msg_el = this.content.lastElementChild;
-                    while (this.isNotPermanentMessage(last_msg_el)) {
-                        last_msg_el = last_msg_el.previousSibling
-                    }
-                    return last_msg_el;
-                },
-
-                getFirstMessageElement () {
-                    let first_msg_el = this.content.firstElementChild;
-                    while (this.isNotPermanentMessage(first_msg_el)) {
-                        first_msg_el = first_msg_el.nextSibling
-                    }
-                    return first_msg_el;
-                },
-
                 getLastMessageDate (cutoff) {
                 getLastMessageDate (cutoff) {
                     /* Return the ISO8601 format date of the latest message.
                     /* Return the ISO8601 format date of the latest message.
                      *
                      *
@@ -454,12 +426,12 @@
                      *  (Object) cutoff: Moment Date cutoff date. The last
                      *  (Object) cutoff: Moment Date cutoff date. The last
                      *      message received cutoff this date will be returned.
                      *      message received cutoff this date will be returned.
                      */
                      */
-                    const first_msg = this.getFirstMessageElement(),
+                    const first_msg = u.getFirstChildElement(this.content, '.message:not(.chat-event)'),
                           oldest_date = first_msg ? first_msg.getAttribute('data-isodate') : null;
                           oldest_date = first_msg ? first_msg.getAttribute('data-isodate') : null;
                     if (!_.isNull(oldest_date) && moment(oldest_date).isAfter(cutoff)) {
                     if (!_.isNull(oldest_date) && moment(oldest_date).isAfter(cutoff)) {
                         return null;
                         return null;
                     }
                     }
-                    const last_msg = this.getLastMessageElement(),
+                    const last_msg = u.getLastChildElement(this.content, '.message:not(.chat-event)'),
                           most_recent_date = last_msg ? last_msg.getAttribute('data-isodate') : null;
                           most_recent_date = last_msg ? last_msg.getAttribute('data-isodate') : null;
                     if (_.isNull(most_recent_date) || moment(most_recent_date).isBefore(cutoff)) {
                     if (_.isNull(most_recent_date) || moment(most_recent_date).isBefore(cutoff)) {
                         return most_recent_date;
                         return most_recent_date;
@@ -511,7 +483,30 @@
                     }
                     }
                     this.insertDayIndicator(message_el);
                     this.insertDayIndicator(message_el);
                     this.clearStatusNotification();
                     this.clearStatusNotification();
-                    this.scrollDown();
+                    this.setScrollPosition(message_el);
+                },
+
+                setScrollPosition (message_el) {
+                    /* Given a newly inserted message, determine whether we
+                     * should keep the scrollbar in place (so as to not scroll
+                     * up when using infinite scroll).
+                     */
+                    if (this.model.get('scrolled')) {
+                        const next_msg_el = u.getNextElement(message_el, ".chat-message");
+                        if (next_msg_el) {
+                            // The currently received message is not new, there
+                            // are newer messages after it. So let's see if we
+                            // should maintain our current scroll position.
+                            if (this.content.scrollTop === 0 || this.model.get('top_visible_message')) {
+                                const top_visible_message = this.model.get('top_visible_message') || next_msg_el;
+
+                                this.model.set('top_visible_message', top_visible_message);
+                                this.content.scrollTop = top_visible_message.offsetTop - 30;
+                            }
+                        }
+                    } else {
+                        this.scrollDown();
+                    }
                 },
                 },
 
 
                 getExtraMessageTemplateAttributes () {
                 getExtraMessageTemplateAttributes () {
@@ -1027,13 +1022,6 @@
                      * received.
                      * received.
                      */
                      */
                     if (ev && ev.preventDefault) { ev.preventDefault(); }
                     if (ev && ev.preventDefault) { ev.preventDefault(); }
-                    if (this.model.get('auto_scrolled')) {
-                        this.model.set({
-                            'scrolled': false,
-                            'auto_scrolled': false
-                        });
-                        return;
-                    }
                     let scrolled = true;
                     let scrolled = true;
                     const is_at_bottom =
                     const is_at_bottom =
                         (this.content.scrollTop + this.content.clientHeight) >=
                         (this.content.scrollTop + this.content.clientHeight) >=
@@ -1043,11 +1031,17 @@
                         scrolled = false;
                         scrolled = false;
                         this.onScrolledDown();
                         this.onScrolledDown();
                     }
                     }
-                    u.safeSave(this.model, {'scrolled': scrolled});
+                    u.safeSave(this.model, {
+                        'scrolled': scrolled,
+                        'top_visible_message': null
+                    });
                 },
                 },
 
 
                 viewUnreadMessages () {
                 viewUnreadMessages () {
-                    this.model.save('scrolled', false);
+                    this.model.save({
+                        'scrolled': false,
+                        'top_visible_message': null
+                    });
                     this.scrollDown();
                     this.scrollDown();
                 },
                 },
 
 
@@ -1058,8 +1052,6 @@
                     }
                     }
                     if (u.isVisible(this.content) && !this.model.get('scrolled')) {
                     if (u.isVisible(this.content) && !this.model.get('scrolled')) {
                         this.content.scrollTop = this.content.scrollHeight;
                         this.content.scrollTop = this.content.scrollHeight;
-                        this.onScrolledDown();
-                        this.model.save({'auto_scrolled': true});
                     }
                     }
                 },
                 },
 
 

+ 98 - 94
src/converse-mam.js

@@ -36,6 +36,83 @@
         }
         }
     }
     }
 
 
+    function queryForArchivedMessages (_converse, options, callback, errback) {
+        /* Internal function, called by the "archive.query" API method.
+         */
+        let date;
+        if (_.isFunction(options)) {
+            callback = options;
+            errback = callback;
+        }
+        const queryid = _converse.connection.getUniqueId();
+        const attrs = {'type':'set'};
+        if (!_.isUndefined(options) && options.groupchat) {
+            if (!options['with']) { // eslint-disable-line dot-notation
+                throw new Error(
+                    'You need to specify a "with" value containing '+
+                    'the chat room JID, when querying groupchat messages.');
+            }
+            attrs.to = options['with']; // eslint-disable-line dot-notation
+        }
+        const stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid});
+        if (!_.isUndefined(options)) {
+            stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'})
+                    .c('field', {'var':'FORM_TYPE', 'type': 'hidden'})
+                    .c('value').t(Strophe.NS.MAM).up().up();
+
+            if (options['with'] && !options.groupchat) {  // eslint-disable-line dot-notation
+                stanza.c('field', {'var':'with'}).c('value')
+                    .t(options['with']).up().up(); // eslint-disable-line dot-notation
+            }
+            _.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 instanceof Strophe.RSM) {
+                stanza.cnode(options.toXML());
+            } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) {
+                stanza.cnode(new Strophe.RSM(options).toXML());
+            }
+        }
+
+        const messages = [];
+        const message_handler = _converse.connection.addHandler(function (message) {
+            const result = message.querySelector('result');
+            if (!_.isNull(result) && result.getAttribute('queryid') === queryid) {
+                messages.push(message);
+            }
+            return true;
+        }, Strophe.NS.MAM);
+
+        _converse.connection.sendIQ(
+            stanza,
+            function (iq) {
+                _converse.connection.deleteHandler(message_handler);
+                if (_.isFunction(callback)) {
+                    const set = iq.querySelector('set');
+                    let rsm;
+                    if (!_.isUndefined(set)) {
+                        rsm = new Strophe.RSM({xml: set});
+                        _.extend(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max'])));
+                    }
+                    callback(messages, rsm);
+                }
+            },
+            function () {
+                _converse.connection.deleteHandler(message_handler);
+                if (_.isFunction(errback)) { errback.apply(this, arguments); }
+            },
+            _converse.message_archiving_timeout
+        );
+    }
+
 
 
     converse.plugins.add('converse-mam', {
     converse.plugins.add('converse-mam', {
 
 
@@ -150,7 +227,7 @@
                         return;
                         return;
                     }
                     }
                     this.addSpinner();
                     this.addSpinner();
-                    _converse.queryForArchivedMessages(
+                    _converse.api.archive.query(
                         _.extend({
                         _.extend({
                             '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,
@@ -283,97 +360,6 @@
                 message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request
                 message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request
             });
             });
 
 
-            _converse.queryForArchivedMessages = 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.
-                 */
-                let date;
-                if (_.isFunction(options)) {
-                    callback = options;
-                    errback = callback;
-                }
-                const queryid = _converse.connection.getUniqueId();
-                const attrs = {'type':'set'};
-                if (!_.isUndefined(options) && options.groupchat) {
-                    if (!options['with']) { // eslint-disable-line dot-notation
-                        throw new Error(
-                            'You need to specify a "with" value containing '+
-                            'the chat room JID, when querying groupchat messages.');
-                    }
-                    attrs.to = options['with']; // eslint-disable-line dot-notation
-                }
-                const stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid});
-                if (!_.isUndefined(options)) {
-                    stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'})
-                            .c('field', {'var':'FORM_TYPE', 'type': 'hidden'})
-                            .c('value').t(Strophe.NS.MAM).up().up();
-
-                    if (options['with'] && !options.groupchat) {  // eslint-disable-line dot-notation
-                        stanza.c('field', {'var':'with'}).c('value')
-                            .t(options['with']).up().up(); // eslint-disable-line dot-notation
-                    }
-                    _.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 instanceof Strophe.RSM) {
-                        stanza.cnode(options.toXML());
-                    } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) {
-                        stanza.cnode(new Strophe.RSM(options).toXML());
-                    }
-                }
-
-                const messages = [];
-                const message_handler = _converse.connection.addHandler(function (message) {
-                    const result = message.querySelector('result');
-                    if (!_.isNull(result) && result.getAttribute('queryid') === queryid) {
-                        messages.push(message);
-                    }
-                    return true;
-                }, Strophe.NS.MAM);
-
-                _converse.connection.sendIQ(
-                    stanza,
-                    function (iq) {
-                        _converse.connection.deleteHandler(message_handler);
-                        if (_.isFunction(callback)) {
-                            const set = iq.querySelector('set');
-                            let rsm;
-                            if (!_.isUndefined(set)) {
-                                rsm = new Strophe.RSM({xml: set});
-                                _.extend(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max'])));
-                            }
-                            callback(messages, rsm);
-                        }
-                    },
-                    function () {
-                        _converse.connection.deleteHandler(message_handler);
-                        if (_.isFunction(errback)) { errback.apply(this, arguments); }
-                    },
-                    _converse.message_archiving_timeout
-                );
-            };
 
 
             _converse.onMAMError = function (iq) {
             _converse.onMAMError = function (iq) {
                 if (iq.querySelectorAll('feature-not-implemented').length) {
                 if (iq.querySelectorAll('feature-not-implemented').length) {
@@ -457,11 +443,29 @@
                 /* Extend default converse.js API to add methods specific to MAM
                 /* Extend default converse.js API to add methods specific to MAM
                  */
                  */
                 'archive': {
                 'archive': {
-                    'query': function () {
+                    '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.
+                         *
+                         * When the 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.
+                         */
                         if (!_converse.api.connection.connected()) {
                         if (!_converse.api.connection.connected()) {
                             throw new Error('Can\'t call `api.archive.query` before having established an XMPP session');
                             throw new Error('Can\'t call `api.archive.query` before having established an XMPP session');
                         }
                         }
-                        return _converse.queryForArchivedMessages.apply(this, arguments);
+                        return queryForArchivedMessages(_converse, options, callback, errback);
                     }
                     }
                 }
                 }
             });
             });

+ 44 - 12
src/utils.js

@@ -64,16 +64,6 @@
         });
         });
     };
     };
 
 
-    function calculateElementHeight (el) {
-        /* Return the height of the passed in DOM element,
-         * based on the heights of its children.
-         */
-        return _.reduce(
-            el.children,
-            (result, child) => result + child.offsetHeight, 0
-        );
-    }
-
     function slideOutWrapup (el) {
     function slideOutWrapup (el) {
         /* Wrapup function for slideOut. */
         /* Wrapup function for slideOut. */
         el.removeAttribute('data-slider-marker');
         el.removeAttribute('data-slider-marker');
@@ -85,6 +75,48 @@
 
 
     var u = {};
     var u = {};
 
 
+    u.getNextElement = function (el, selector='*') {
+        let next_el = el.nextElementSibling;
+        while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
+            next_el = next_el.nextElementSibling;
+        }
+        return next_el;
+    }
+
+    u.getPreviousElement = function (el, selector='*') {
+        let prev_el = el.previousSibling;
+        while (!_.isNull(prev_el) && !sizzle.matchesSelector(prev_el, selector)) {
+            prev_el = prev_el.previousSibling
+        }
+        return prev_el;
+    }
+
+    u.getFirstChildElement = function (el, selector='*') {
+        let first_el = el.firstElementChild;
+        while (!_.isNull(first_el) && !sizzle.matchesSelector(first_el, selector)) {
+            first_el = first_el.nextSibling
+        }
+        return first_el;
+    }
+
+    u.getLastChildElement = function (el, selector='*') {
+        let last_el = el.lastElementChild;
+        while (!_.isNull(last_el) && !sizzle.matchesSelector(last_el, selector)) {
+            last_el = last_el.previousSibling
+        }
+        return last_el;
+    }
+
+    u.calculateElementHeight = function (el) {
+        /* Return the height of the passed in DOM element,
+         * based on the heights of its children.
+         */
+        return _.reduce(
+            el.children,
+            (result, child) => result + child.offsetHeight, 0
+        );
+    }
+
     u.addClass = function (className, el) {
     u.addClass = function (className, el) {
         if (el instanceof Element) {
         if (el instanceof Element) {
             el.classList.add(className);
             el.classList.add(className);
@@ -199,7 +231,7 @@
                 el.removeAttribute('data-slider-marker');
                 el.removeAttribute('data-slider-marker');
                 window.cancelAnimationFrame(marker);
                 window.cancelAnimationFrame(marker);
             }
             }
-            const end_height = calculateElementHeight(el);
+            const end_height = u.calculateElementHeight(el);
             if (window.converse_disable_effects) { // Effects are disabled (for tests)
             if (window.converse_disable_effects) { // Effects are disabled (for tests)
                 el.style.height = end_height + 'px';
                 el.style.height = end_height + 'px';
                 slideOutWrapup(el);
                 slideOutWrapup(el);
@@ -227,7 +259,7 @@
                     // browser bug where browsers don't know the correct
                     // browser bug where browsers don't know the correct
                     // offsetHeight beforehand.
                     // offsetHeight beforehand.
                     el.removeAttribute('data-slider-marker');
                     el.removeAttribute('data-slider-marker');
-                    el.style.height = calculateElementHeight(el) + 'px';
+                    el.style.height = u.calculateElementHeight(el) + 'px';
                     el.style.overflow = "";
                     el.style.overflow = "";
                     el.style.height = "";
                     el.style.height = "";
                     resolve();
                     resolve();