Merge remote-tracking branch 'origin/master'

This commit is contained in:
Weblate 2018-01-03 14:37:36 +01:00
commit 3a0dac3ada
5 changed files with 331 additions and 203 deletions

View File

@ -34,6 +34,7 @@
- Show status messages in an MUC room when a user's role changes. - Show status messages in an MUC room when a user's role changes.
- In MUC chat rooms, collapse multiple, consecutive join/leave messages. - In MUC chat rooms, collapse multiple, consecutive join/leave messages.
- Performance improvements for rendering private chats, rooms and the contacts roster. - Performance improvements for rendering private chats, rooms and the contacts roster.
- MUC Leave/Join messages now also show a new day indicator if applicable.
### API changes ### API changes
- New API method `_converse.disco.supports` to check whether a certain - New API method `_converse.disco.supports` to check whether a certain

View File

@ -879,6 +879,60 @@
}).then(done); }).then(done);
})); }));
it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.createContacts(_converse, 'current');
test_utils.openControlBox();
test_utils.openContactsPanel(_converse);
test_utils.waitUntil(function () {
return _converse.rosterview.$el.find('.roster-group').length;
}, 300)
.then(function () {
// Send a message from a different resource
var message, sender_jid, msg;
spyOn(_converse, 'log');
spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
_converse.filter_by_resource = true;
sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
msg = $msg({
from: sender_jid,
to: _converse.bare_jid+"/some-other-resource",
type: 'chat',
id: (new Date()).getTime()
}).c('body').t("This message will not be shown").up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
_converse.chatboxes.onMessage(msg);
expect(_converse.log).toHaveBeenCalledWith(
"onMessage: Ignoring incoming message intended for a different resource: dummy@localhost/some-other-resource",
Strophe.LogLevel.INFO);
expect(_converse.chatboxes.getChatBox).not.toHaveBeenCalled();
_converse.filter_by_resource = false;
message = "This message sent to a different resource will be shown";
msg = $msg({
from: sender_jid,
to: _converse.bare_jid+"/some-other-resource",
type: 'chat',
id: '134234623462346'
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
_converse.chatboxes.onMessage(msg);
expect(_converse.chatboxes.getChatBox).toHaveBeenCalled();
var chatboxview = _converse.chatboxviews.get(sender_jid);
var $chat_content = chatboxview.$el.find('.chat-content:last');
var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
done();
});
}));
});
it("can be received out of order, and will still be displayed in the right order", it("can be received out of order, and will still be displayed in the right order",
mock.initConverseWithPromises( mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},
@ -1017,60 +1071,6 @@
}); });
})); }));
it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.createContacts(_converse, 'current');
test_utils.openControlBox();
test_utils.openContactsPanel(_converse);
test_utils.waitUntil(function () {
return _converse.rosterview.$el.find('.roster-group').length;
}, 300)
.then(function () {
// Send a message from a different resource
var message, sender_jid, msg;
spyOn(_converse, 'log');
spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
_converse.filter_by_resource = true;
sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
msg = $msg({
from: sender_jid,
to: _converse.bare_jid+"/some-other-resource",
type: 'chat',
id: (new Date()).getTime()
}).c('body').t("This message will not be shown").up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
_converse.chatboxes.onMessage(msg);
expect(_converse.log).toHaveBeenCalledWith(
"onMessage: Ignoring incoming message intended for a different resource: dummy@localhost/some-other-resource",
Strophe.LogLevel.INFO);
expect(_converse.chatboxes.getChatBox).not.toHaveBeenCalled();
_converse.filter_by_resource = false;
message = "This message sent to a different resource will be shown";
msg = $msg({
from: sender_jid,
to: _converse.bare_jid+"/some-other-resource",
type: 'chat',
id: '134234623462346'
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
_converse.chatboxes.onMessage(msg);
expect(_converse.chatboxes.getChatBox).toHaveBeenCalled();
var chatboxview = _converse.chatboxviews.get(sender_jid);
var $chat_content = chatboxview.$el.find('.chat-content:last');
var msg_txt = $chat_content.find('.chat-message').find('.chat-msg-content').text();
expect(msg_txt).toEqual(message);
done();
});
}));
});
it("is ignored if it's a malformed headline message", it("is ignored if it's a malformed headline message",
mock.initConverseWithPromises( mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},

View File

@ -7,6 +7,7 @@
var $msg = converse.env.$msg; var $msg = converse.env.$msg;
var Strophe = converse.env.Strophe; var Strophe = converse.env.Strophe;
var Promise = converse.env.Promise; var Promise = converse.env.Promise;
var moment = converse.env.moment;
return describe("ChatRooms", function () { return describe("ChatRooms", function () {
describe("The \"rooms\" API", function () { describe("The \"rooms\" API", function () {
@ -591,6 +592,147 @@
done(); done();
})); }));
it("shows a new day indicator if a join/leave message is received on a new day",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.openChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1');
var view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
var $chat_content = view.$el.find('.chat-content');
/* <presence to="dummy@localhost/_converse.js-29092160"
* from="coven@chat.shakespeare.lit/some1">
* <x xmlns="http://jabber.org/protocol/muc#user">
* <item affiliation="owner" jid="dummy@localhost/_converse.js-29092160" role="moderator"/>
* <status code="110"/>
* </x>
* </presence></body>
*/
var presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
from: 'coven@chat.shakespeare.lit/some1'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'owner',
'jid': 'dummy@localhost/_converse.js-29092160',
'role': 'moderator'
}).up()
.c('status', {code: '110'});
_converse.connection._dataRecv(test_utils.createRequest(presence));
var $time = $chat_content.find('time');
expect($time.length).toEqual(1);
expect($time.attr('class')).toEqual('chat-info chat-date');
expect($time.data('isodate')).toEqual(moment().startOf('day').format());
expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY"));
expect($chat_content.find('div.chat-info:first').html()).toBe("some1 has entered the room.");
// XXX: Hack. We clear the chat contents instead of mocking the date
$chat_content.html('');
// Test a user leaving a chat room
presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
type: 'unavailable',
from: 'coven@chat.shakespeare.lit/some1'
})
.c('status', 'Disconnected: Replaced by new connection').up()
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'some1@localhost/_converse.js-290929789',
'role': 'none'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
$time = $chat_content.find('time');
expect($time.length).toEqual(1);
expect($time.attr('class')).toEqual('chat-info chat-date');
expect($time.data('isodate')).toEqual(moment().startOf('day').format());
expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY"));
expect($chat_content.find('div.chat-info').length).toBe(1);
expect($chat_content.find('div.chat-info:last').html()).toBe(
'some1 has left the room. '+
'"Disconnected: Replaced by new connection"');
// XXX: Hack. We clear the chat contents instead of mocking the date
$chat_content.html('');
var stanza = Strophe.xmlHtmlNode(
'<message xmlns="jabber:client"' +
' to="dummy@localhost/_converse.js-290929789"' +
' type="groupchat"' +
' from="coven@chat.shakespeare.lit/some1">'+
' <body>hello world</body>'+
' <delay xmlns="urn:xmpp:delay" stamp="2018-01-01T09:35:39Z" from="some1@localhost"/>'+
'</message>').firstChild;
_converse.connection._dataRecv(test_utils.createRequest(stanza));
presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
from: 'coven@chat.shakespeare.lit/newguy'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'newguy@localhost/_converse.js-290929789',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
$time = $chat_content.find('time');
expect($time.length).toEqual(2);
$time = $chat_content.find('time:eq(1)');
expect($time.attr('class')).toEqual('chat-info chat-date');
expect($time.data('isodate')).toEqual(moment().startOf('day').format());
expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY"));
expect($chat_content.find('div.chat-info').length).toBe(1);
expect($chat_content.find('div.chat-info:first').html()).toBe("newguy has entered the room.");
// XXX: Hack. We clear the chat contents instead of mocking the date
$chat_content.html('');
stanza = Strophe.xmlHtmlNode(
'<message xmlns="jabber:client"' +
' to="dummy@localhost/_converse.js-290929789"' +
' type="groupchat"' +
' from="coven@chat.shakespeare.lit/some1">'+
' <body>hello world</body>'+
' <delay xmlns="urn:xmpp:delay" stamp="2018-01-01T09:35:39Z" from="some1@localhost"/>'+
'</message>').firstChild;
_converse.connection._dataRecv(test_utils.createRequest(stanza));
// Test a user leaving a chat room
presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
type: 'unavailable',
from: 'coven@chat.shakespeare.lit/some1'
})
.c('status', 'Disconnected: Replaced by new connection').up()
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'some1@localhost/_converse.js-290929789',
'role': 'none'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
$time = $chat_content.find('time');
expect($time.length).toEqual(2);
$time = $chat_content.find('time:eq(1)');
expect($time.attr('class')).toEqual('chat-info chat-date');
expect($time.data('isodate')).toEqual(moment().startOf('day').format());
expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY"));
expect($chat_content.find('div.chat-info').length).toBe(1);
expect($chat_content.find('div.chat-info:last').html()).toBe(
'some1 has left the room. '+
'"Disconnected: Replaced by new connection"');
done();
return;
}));
it("shows its description in the chat heading", it("shows its description in the chat heading",
mock.initConverseWithPromises( mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},
@ -1863,7 +2005,7 @@
textarea.textContent = '/help This is the room subject'; textarea.textContent = '/help This is the room subject';
$(textarea).trigger($.Event('keypress', {keyCode: 13})); $(textarea).trigger($.Event('keypress', {keyCode: 13}));
expect(view.onMessageSubmitted).toHaveBeenCalled(); expect(view.onMessageSubmitted).toHaveBeenCalled();
const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info'), 0); const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info:not(.chat-date)'), 0);
expect(info_messages.length).toBe(17); expect(info_messages.length).toBe(17);
expect(info_messages.pop().textContent).toBe('/voice: Allow muted user to post messages'); expect(info_messages.pop().textContent).toBe('/voice: Allow muted user to post messages');
expect(info_messages.pop().textContent).toBe('/topic: Set room subject (alias for /subject)'); expect(info_messages.pop().textContent).toBe('/topic: Set room subject (alias for /subject)');
@ -2105,7 +2247,7 @@
.c('status', {'code': '307'}); .c('status', {'code': '307'});
_converse.connection._dataRecv(test_utils.createRequest(presence)); _converse.connection._dataRecv(test_utils.createRequest(presence));
expect( expect(
view.el.querySelectorAll('.chat-info')[2].textContent).toBe( view.el.querySelectorAll('.chat-info')[3].textContent).toBe(
"annoyingGuy has been kicked out"); "annoyingGuy has been kicked out");
done(); done();
}); });

View File

@ -382,37 +382,41 @@
); );
}, },
insertDayIndicator (date, insert_method) { insertDayIndicator (next_msg_el) {
/* Inserts an indicator /* Inserts an indicator into the chat area, showing the
* into the chat area, showing the day as given by the * day as given by the passed in date.
* passed in date. *
* The indicator is only inserted if necessary.
* *
* Parameters: * Parameters:
* (String) date - An ISO8601 date string. * (HTMLElement) next_msg_el - The message element before
* (Function) insert_method - The method to be used to * which the day indicator element must be inserted.
* insert the indicator * This element must have a "data-isodate" attribute
* which specifies its creation date.
*/ */
const day_date = moment(date).startOf('day'); const prev_msg_el = this.getPreviousMessageElement(next_msg_el),
insert_method(tpl_new_day({ prev_msg_date = _.isNull(prev_msg_el) ? null : prev_msg_el.getAttribute('data-isodate'),
isodate: day_date.format(), next_msg_date = next_msg_el.getAttribute('data-isodate');
datestring: day_date.format("dddd MMM Do YYYY")
})); if (_.isNull(prev_msg_date) || moment(next_msg_date).isAfter(prev_msg_date, 'day')) {
const day_date = moment(next_msg_date).startOf('day');
next_msg_el.insertAdjacentHTML('beforeBegin',
tpl_new_day({
'isodate': day_date.format(),
'datestring': day_date.format("dddd MMM Do YYYY")
})
);
}
}, },
insertMessage (attrs, insert_method) { getPreviousMessageElement (el) {
/* Helper method which appends a message (or prepends if the let prev_msg_el = el.previousSibling;
* 2nd parameter is set to true) to the end of the chat box's while (!_.isNull(prev_msg_el) &&
* content area. !u.hasClass(prev_msg_el, 'message') &&
* !u.hasClass(prev_msg_el, 'chat-info')) {
* Parameters: prev_msg_el = prev_msg_el.previousSibling
* (Object) attrs: An object containing the message attributes. }
*/ return prev_msg_el;
_.flow((el) => {
insert_method(el);
return el;
},
this.scrollDown.bind(this)
)(this.renderMessage(attrs));
}, },
getLastMessageElement () { getLastMessageElement () {
@ -469,11 +473,6 @@
} }
}, },
getDayIndicatorElement (date) {
return this.content.querySelector(
`.chat-date[data-isodate="${date.startOf('day').format()}"]`);
},
showMessage (attrs) { showMessage (attrs) {
/* Inserts a chat message into the content area of the chat box. /* Inserts a chat message into the content area of the chat box.
* Will also insert a new day indicator if the message is on a * Will also insert a new day indicator if the message is on a
@ -488,37 +487,17 @@
*/ */
const current_msg_date = moment(attrs.time) || moment, const current_msg_date = moment(attrs.time) || moment,
prepend_html = _.bind(this.content.insertAdjacentHTML, this.content, 'afterbegin'), prepend_html = _.bind(this.content.insertAdjacentHTML, this.content, 'afterbegin'),
previous_msg_date = this.getLastMessageDate(current_msg_date); previous_msg_date = this.getLastMessageDate(current_msg_date),
message_el = this.renderMessage(attrs);
if (_.isNull(previous_msg_date)) { if (_.isNull(previous_msg_date)) {
this.insertMessage(attrs, _.bind(this.content.insertAdjacentElement, this.content, 'afterbegin')); this.content.insertAdjacentElement('afterbegin', message_el);
this.insertDayIndicator(current_msg_date, prepend_html);
} else { } else {
const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop(); const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop();
const day_el = this.getDayIndicatorElement(current_msg_date) previous_msg_el.insertAdjacentElement('afterend', message_el);
if (current_msg_date.isAfter(previous_msg_date, 'day')) {
if (_.isNull(day_el)) {
this.insertMessage(
attrs,
_.bind(previous_msg_el.insertAdjacentElement, previous_msg_el, 'afterend')
);
this.insertDayIndicator(
current_msg_date,
_.bind(this.content.insertAdjacentHTML, previous_msg_el, 'afterend')
);
} else {
this.insertMessage(
attrs,
_.bind(previous_msg_el.insertAdjacentElement, day_el, 'afterend')
);
}
} else {
this.insertMessage(
attrs,
_.bind(previous_msg_el.insertAdjacentElement, previous_msg_el, 'afterend')
);
}
} }
this.insertDayIndicator(message_el);
this.scrollDown();
}, },
getExtraMessageTemplateAttributes () { getExtraMessageTemplateAttributes () {

View File

@ -1837,6 +1837,7 @@
const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); const nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
const stat = stanza.querySelector('status'); const stat = stanza.querySelector('status');
const last_el = this.content.lastElementChild; const last_el = this.content.lastElementChild;
if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && if (_.includes(_.get(last_el, 'classList', []), 'chat-info') &&
_.get(last_el, 'dataset', {}).leave === `"${nick}"`) { _.get(last_el, 'dataset', {}).leave === `"${nick}"`) {
last_el.outerHTML = last_el.outerHTML =
@ -1860,7 +1861,9 @@
last_el.outerHTML = tpl_info(data); last_el.outerHTML = tpl_info(data);
} else { } else {
this.content.insertAdjacentHTML('beforeend', tpl_info(data)); const el = u.stringToElement(tpl_info(data));
this.content.insertAdjacentElement('beforeend', el);
this.insertDayIndicator(el);
} }
} }
this.scrollDown(); this.scrollDown();
@ -1890,6 +1893,7 @@
} }
const data = { const data = {
'message': message, 'message': message,
'isodate': moment().format(),
'data': `data-leave="${nick}"` 'data': `data-leave="${nick}"`
} }
if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && if (_.includes(_.get(last_el, 'classList', []), 'chat-info') &&
@ -1897,7 +1901,9 @@
last_el.outerHTML = tpl_info(data); last_el.outerHTML = tpl_info(data);
} else { } else {
this.content.insertAdjacentHTML('beforeend', tpl_info(data)); const el = u.stringToElement(tpl_info(data));
this.content.insertAdjacentElement('beforeend', el);
this.insertDayIndicator(el);
} }
} }
this.scrollDown(); this.scrollDown();