Commit 26d3455f authored by Weblate's avatar Weblate

Merge remote-tracking branch 'origin/master'

parents dcf87368 2cb4a36a
......@@ -47,6 +47,7 @@
- `hide_open_bookmarks` is now by default `true`.
### UX/UI changes
- #984 Improve loading of archived messages via "infinite scroll"
- Use CSS3 fade transitions to render various elements.
- Remove `Login` and `Registration` tabs and consolidate into one panel.
- Show validation error messages on the login form.
......
......@@ -404,7 +404,7 @@
* This element must have a "data-isodate" attribute
* 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'),
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) {
/* Return the ISO8601 format date of the latest message.
*
......@@ -454,12 +426,12 @@
* (Object) cutoff: Moment Date cutoff date. The last
* 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;
if (!_.isNull(oldest_date) && moment(oldest_date).isAfter(cutoff)) {
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;
if (_.isNull(most_recent_date) || moment(most_recent_date).isBefore(cutoff)) {
return most_recent_date;
......@@ -511,7 +483,30 @@
}
this.insertDayIndicator(message_el);
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 () {
......@@ -1027,13 +1022,6 @@
* received.
*/
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (this.model.get('auto_scrolled')) {
this.model.set({
'scrolled': false,
'auto_scrolled': false
});
return;
}
let scrolled = true;
const is_at_bottom =
(this.content.scrollTop + this.content.clientHeight) >=
......@@ -1043,11 +1031,17 @@
scrolled = false;
this.onScrolledDown();
}
u.safeSave(this.model, {'scrolled': scrolled});
u.safeSave(this.model, {
'scrolled': scrolled,
'top_visible_message': null
});
},
viewUnreadMessages () {
this.model.save('scrolled', false);
this.model.save({
'scrolled': false,
'top_visible_message': null
});
this.scrollDown();
},
......@@ -1058,8 +1052,6 @@
}
if (u.isVisible(this.content) && !this.model.get('scrolled')) {
this.content.scrollTop = this.content.scrollHeight;
this.onScrolledDown();
this.model.save({'auto_scrolled': true});
}
},
......
......@@ -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', {
......@@ -150,7 +227,7 @@
return;
}
this.addSpinner();
_converse.queryForArchivedMessages(
_converse.api.archive.query(
_.extend({
'before': '', // Page backwards from the most recent message
'max': _converse.archived_messages_page_size,
......@@ -283,97 +360,6 @@
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) {
if (iq.querySelectorAll('feature-not-implemented').length) {
......@@ -457,11 +443,29 @@
/* Extend default converse.js API to add methods specific to MAM
*/
'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()) {
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);
}
}
});
......
......@@ -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) {
/* Wrapup function for slideOut. */
el.removeAttribute('data-slider-marker');
......@@ -85,6 +75,48 @@
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) {
if (el instanceof Element) {
el.classList.add(className);
......@@ -199,7 +231,7 @@
el.removeAttribute('data-slider-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)
el.style.height = end_height + 'px';
slideOutWrapup(el);
......@@ -227,7 +259,7 @@
// browser bug where browsers don't know the correct
// offsetHeight beforehand.
el.removeAttribute('data-slider-marker');
el.style.height = calculateElementHeight(el) + 'px';
el.style.height = u.calculateElementHeight(el) + 'px';
el.style.overflow = "";
el.style.height = "";
resolve();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment