Also squash leave/join messages

And fix an HTML rendering bug for info messages and nicks that contain spaces
This commit is contained in:
JC Brand 2018-10-11 18:58:13 +02:00
parent 71cc98d6f6
commit 7b9c97dfd3
6 changed files with 6608 additions and 6126 deletions

12505
dist/converse.js vendored

File diff suppressed because one or more lines are too long

View File

@ -558,7 +558,7 @@
expect($chat_content.find('div.chat-info').length).toBe(4); expect($chat_content.find('div.chat-info').length).toBe(4);
var $msg_el = $chat_content.find('div.chat-info:last'); var $msg_el = $chat_content.find('div.chat-info:last');
expect($msg_el.html()).toBe("newguy has left and re-entered the groupchat"); expect($msg_el.html()).toBe("newguy has left and re-entered the groupchat");
expect($msg_el.data('leavejoin')).toBe('"newguy"'); expect($msg_el.data('leavejoin')).toBe('newguy');
presence = $pres({ presence = $pres({
to: 'dummy@localhost/_converse.js-29092160', to: 'dummy@localhost/_converse.js-29092160',
@ -575,7 +575,7 @@
expect($chat_content.find('div.chat-info').length).toBe(4); expect($chat_content.find('div.chat-info').length).toBe(4);
const msg_el = sizzle('div.chat-info', chat_content).pop(); const msg_el = sizzle('div.chat-info', chat_content).pop();
expect(msg_el.textContent).toBe('newguy has left the groupchat'); expect(msg_el.textContent).toBe('newguy has left the groupchat');
expect(msg_el.getAttribute('data-leave')).toBe('"newguy"'); expect(msg_el.getAttribute('data-leave')).toBe('newguy');
presence = $pres({ presence = $pres({
to: 'dummy@localhost/_converse.js-29092160', to: 'dummy@localhost/_converse.js-29092160',
@ -687,6 +687,160 @@
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)) }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
})); }));
it("combines subsequent join/leave messages when users enter or exit a groupchat",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
function (done, _converse) {
test_utils.openAndEnterChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'dummy')
.then(() => {
const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
const chat_content = view.el.querySelector('.chat-content');
expect(sizzle('div.chat-info', chat_content).length).toBe(1);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("dummy has entered the groupchat");
let presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" from="coven@chat.shakespeare.lit/fabio">
<c xmlns="http://jabber.org/protocol/caps" node="http://conversations.im" ver="INI3xjRUioclBTP/aACfWi5m9UY=" hash="sha-1"/>
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="fabio@montefuscolo.com.br/Conversations.ZvLu" role="participant"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(2);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("fabio has entered the groupchat");
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" from="coven@chat.shakespeare.lit/Dele Olajide">
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="deleo@traderlynk.4ng.net/converse.js-39320524" role="participant"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(3);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" from="coven@chat.shakespeare.lit/jcbrand">
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="owner" jid="jc@opkode.com/converse.js-30645022" role="moderator"/>
<status code="110"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(4);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("jcbrand has entered the groupchat");
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" type="unavailable" from="coven@chat.shakespeare.lit/Dele Olajide">
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="deleo@traderlynk.4ng.net/converse.js-39320524" role="none"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(4);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("Dele Olajide has entered and left the groupchat");
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" from="coven@chat.shakespeare.lit/Dele Olajide">
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="deleo@traderlynk.4ng.net/converse.js-74567907" role="participant"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(4);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("Dele Olajide has entered the groupchat");
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" from="coven@chat.shakespeare.lit/fuvuv" xml:lang="en">
<c xmlns="http://jabber.org/protocol/caps" node="http://jabber.pix-art.de" ver="5tOurnuFnp2h50hKafeUyeN4Yl8=" hash="sha-1"/>
<x xmlns="vcard-temp:x:update"/>
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="fuvuv@blabber.im/Pix-Art Messenger.8zoB" role="participant"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(5);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("fuvuv has entered the groupchat");
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" type="unavailable" from="coven@chat.shakespeare.lit/fuvuv">
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="fuvuv@blabber.im/Pix-Art Messenger.8zoB" role="none"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(5);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe("fuvuv has entered and left the groupchat");
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" type="unavailable" from="coven@chat.shakespeare.lit/fabio">
<status>Disconnected: Replaced by new connection</status>
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="fabio@montefuscolo.com.br/Conversations.ZvLu" role="none"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(5);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
`fabio has entered and left the groupchat. "Disconnected: Replaced by new connection"`);
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" from="coven@chat.shakespeare.lit/fabio">
<c xmlns="http://jabber.org/protocol/caps" node="http://conversations.im" ver="INI3xjRUioclBTP/aACfWi5m9UY=" hash="sha-1"/>
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="fabio@montefuscolo.com.br/Conversations.ZvLu" role="participant"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(5);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
`fabio has entered the groupchat`);
// XXX: hack so that we can test leave/enter of occupants
// who were already in the room when we joined.
chat_content.innerHTML = '';
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" type="unavailable" from="coven@chat.shakespeare.lit/fabio">
<status>Disconnected: closed</status>
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="fabio@montefuscolo.com.br/Conversations.ZvLu" role="none"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(1);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
`fabio has left the groupchat. "Disconnected: closed"`);
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" type="unavailable" from="coven@chat.shakespeare.lit/Dele Olajide">
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="deleo@traderlynk.4ng.net/converse.js-74567907" role="none"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(2);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
`Dele Olajide has left the groupchat`);
presence = Strophe.xmlHtmlNode(
`<presence xmlns="jabber:client" to="dummy@localhost/resource" from="coven@chat.shakespeare.lit/fabio">
<c xmlns="http://jabber.org/protocol/caps" node="http://conversations.im" ver="INI3xjRUioclBTP/aACfWi5m9UY=" hash="sha-1"/>
<x xmlns="http://jabber.org/protocol/muc#user">
<item affiliation="none" jid="fabio@montefuscolo.com.br/Conversations.ZvLu" role="participant"/>
</x>
</presence>`).firstElementChild;
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(sizzle('div.chat-info', chat_content).length).toBe(2);
expect(sizzle('div.chat-info:last', chat_content).pop().textContent).toBe(
`fabio has left and re-entered the groupchat`);
done();
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
}));
it("shows a new day indicator if a join/leave message is received on a new day", it("shows a new day indicator if a join/leave message is received on a new day",
mock.initConverseWithPromises( mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},

View File

@ -499,7 +499,7 @@
return this; return this;
}, },
showChatEvent (message, data='') { showChatEvent (message) {
const isodate = moment().format(); const isodate = moment().format();
this.content.insertAdjacentHTML( this.content.insertAdjacentHTML(
'beforeend', 'beforeend',
@ -507,7 +507,6 @@
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'message': message, 'message': message,
'isodate': isodate, 'isodate': isodate,
'data': data
})); }));
this.insertDayIndicator(this.content.lastElementChild); this.insertDayIndicator(this.content.lastElementChild);
this.scrollDown(); this.scrollDown();

View File

@ -163,8 +163,7 @@
msg = u.stringToElement( msg = u.stringToElement(
tpl_info(_.extend(this.model.toJSON(), { tpl_info(_.extend(this.model.toJSON(), {
'extra_classes': 'chat-error', 'extra_classes': 'chat-error',
'isodate': moment_time.format(), 'isodate': moment_time.format()
'data': ''
}))); })));
return this.replaceElement(msg); return this.replaceElement(msg);
}, },

View File

@ -1462,7 +1462,6 @@
this.content.insertAdjacentHTML( this.content.insertAdjacentHTML(
'beforeend', 'beforeend',
tpl_info({ tpl_info({
'data': '',
'isodate': moment().format(), 'isodate': moment().format(),
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'message': message 'message': message
@ -1505,19 +1504,22 @@
} }
const nick = occupant.get('nick'), const nick = occupant.get('nick'),
stat = occupant.get('status'), stat = occupant.get('status'),
last_el = this.content.lastElementChild; last_leave_el = this.getImmediateNotification(this.content.lastElementChild, nick, 'leave');
if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && if (_.includes(_.get(last_leave_el, 'classList', []), 'chat-info') &&
_.get(last_el, 'dataset', {}).leave === `"${nick}"`) { _.get(last_leave_el, 'dataset', {}).leave === nick) {
last_el.outerHTML = let el = this.content.lastElementChild;
el.insertAdjacentElement('afterend', last_leave_el);
last_leave_el.outerHTML =
tpl_info({ tpl_info({
'data': `data-leavejoin="${nick}"`, 'data_name': 'leavejoin',
'data_value': nick,
'isodate': moment().format(), 'isodate': moment().format(),
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'message': __('%1$s has left and re-entered the groupchat', nick) 'message': __('%1$s has left and re-entered the groupchat', nick)
}); });
const el = this.content.lastElementChild; el = this.content.lastElementChild;
setTimeout(() => u.addClass('fade-out', el), 5000); setTimeout(() => u.addClass('fade-out', el), 5000);
setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5250); setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5250);
} else { } else {
@ -1528,15 +1530,16 @@
message = __('%1$s has entered the groupchat. "%2$s"', nick, stat); message = __('%1$s has entered the groupchat. "%2$s"', nick, stat);
} }
const data = { const data = {
'data': `data-join="${nick}"`, 'data_name': 'join',
'data_value': nick,
'isodate': moment().format(), 'isodate': moment().format(),
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'message': message 'message': message
}; };
if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && if (_.includes(_.get(last_leave_el, 'classList', []), 'chat-info') &&
_.get(last_el, 'dataset', {}).joinleave === `"${nick}"`) { _.get(last_leave_el, 'dataset', {}).joinleave === nick) {
last_el.outerHTML = tpl_info(data); last_leave_el.outerHTML = tpl_info(data);
} else { } else {
const el = u.stringToElement(tpl_info(data)); const el = u.stringToElement(tpl_info(data));
this.content.insertAdjacentElement('beforeend', el); this.content.insertAdjacentElement('beforeend', el);
@ -1546,15 +1549,24 @@
this.scrollDown(); this.scrollDown();
}, },
getImmediateJoinNotification (el, nick) { getImmediateNotification (el, nick, type='join') {
while (!_.isNil(el)) { while (!_.isNil(el)) {
const data = _.get(el, 'dataset', {}); const data = _.get(el, 'dataset', {});
if (!_.includes(_.get(el, 'classList', []), 'chat-info')) { if (!_.includes(_.get(el, 'classList', []), 'chat-info')) {
return; return;
} }
if (moment(el.getAttribute('data-isodate')).isSame(new Date(), "day") && if (!moment(el.getAttribute('data-isodate')).isSame(new Date(), "day")) {
(data.join === `"${nick}"` || data.leavejoin === `"${nick}"`)) { el = el.previousElementSibling;
return el; continue;
}
if (type === 'join') {
if (data.join === nick || data.leavejoin === nick) {
return el;
}
} else {
if (data.leave === nick || data.joinleave === nick) {
return el;
}
} }
el = el.previousElementSibling; el = el.previousElementSibling;
} }
@ -1566,12 +1578,12 @@
} }
const nick = occupant.get('nick'), const nick = occupant.get('nick'),
stat = occupant.get('status'), stat = occupant.get('status'),
last_join_el = this.getImmediateJoinNotification(this.content.lastElementChild, nick), last_join_el = this.getImmediateNotification(this.content.lastElementChild, nick, 'join'),
data = _.get(last_join_el, 'dataset', {}); data = _.get(last_join_el, 'dataset', {});
if (last_join_el) { if (last_join_el) {
let message; let message;
if (data.join === `"${nick}"`) { if (data.join === nick) {
if (_.isNil(stat)) { if (_.isNil(stat)) {
message = __('%1$s has entered and left the groupchat', nick); message = __('%1$s has entered and left the groupchat', nick);
} else { } else {
@ -1581,7 +1593,8 @@
el.insertAdjacentElement('afterend', last_join_el); el.insertAdjacentElement('afterend', last_join_el);
last_join_el.outerHTML = last_join_el.outerHTML =
tpl_info({ tpl_info({
'data': `data-joinleave="${nick}"`, 'data_name': 'joinleave',
'data_value': nick,
'isodate': moment().format(), 'isodate': moment().format(),
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'message': message 'message': message
@ -1589,7 +1602,7 @@
el = this.content.lastElementChild; el = this.content.lastElementChild;
setTimeout(() => u.addClass('fade-out', el), 5000); setTimeout(() => u.addClass('fade-out', el), 5000);
setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5250); setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5250);
} else if (data.leavejoin === `"${nick}"`) { } else if (data.leavejoin === nick) {
if (_.isNil(stat)) { if (_.isNil(stat)) {
message = __('%1$s has left the groupchat', nick); message = __('%1$s has left the groupchat', nick);
} else { } else {
@ -1597,7 +1610,8 @@
} }
last_join_el.outerHTML = last_join_el.outerHTML =
tpl_info({ tpl_info({
'data': `data-leave="${nick}"`, 'data_name': 'leave',
'data_value': nick,
'isodate': moment().format(), 'isodate': moment().format(),
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'message': message 'message': message
@ -1614,7 +1628,8 @@
'message': message, 'message': message,
'isodate': moment().format(), 'isodate': moment().format(),
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'data': `data-leave="${nick}"` 'data_name': 'leave',
'data_value': nick
} }
const el = u.stringToElement(tpl_info(data)); const el = u.stringToElement(tpl_info(data));
this.content.insertAdjacentElement('beforeend', el); this.content.insertAdjacentElement('beforeend', el);
@ -1727,7 +1742,6 @@
this.content.insertAdjacentHTML( this.content.insertAdjacentHTML(
'beforeend', 'beforeend',
tpl_info({ tpl_info({
'data': '',
'isodate': date, 'isodate': date,
'extra_classes': 'chat-event', 'extra_classes': 'chat-event',
'message': message 'message': message
@ -1737,7 +1751,6 @@
this.content.insertAdjacentHTML( this.content.insertAdjacentHTML(
'beforeend', 'beforeend',
tpl_info({ tpl_info({
'data': '',
'isodate': date, 'isodate': date,
'extra_classes': 'chat-topic', 'extra_classes': 'chat-topic',
'message': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})), 'message': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),

View File

@ -1,10 +1,8 @@
{[ if (o.render_message) { ]} {[ if (o.render_message) { ]}
<!-- XXX: Should only ever be rendered if the message text has been sanitized already --> <!-- XXX: Should only ever be rendered if the message text has been sanitized already -->
<div class="message chat-info {{{o.extra_classes}}}" <div class="message chat-info {{{o.extra_classes}}}"
data-isodate="{{{o.isodate}}}" data-isodate="{{{o.isodate}}}" {[ if (o.data_name) { ]} data-{{{o.data_name}}}="{{{o.data_value}}}"{[ } ]}>{{o.message}}</div>
{{{o.data}}}>{{o.message}}</div>
{[ } else { ]} {[ } else { ]}
<div class="message chat-info {{{o.extra_classes}}}" <div class="message chat-info {{{o.extra_classes}}}"
data-isodate="{{{o.isodate}}}" data-isodate="{{{o.isodate}}}" {[ if (o.data_name) { ]} data-{{{o.data_name}}}="{{{o.data_value}}}"{[ } ]}>{{{o.message}}}</div>
{{{o.data}}}>{{{o.message}}}</div>
{[ } ]} {[ } ]}