MUC: Don't show topic change notification for old changes.

Also, show topic by creating a message object, instead of directly
inserting HTML into the DOM. This is a necessary precursor to being able
to render chat messages via lit-html/lit-element.
This commit is contained in:
JC Brand 2020-04-07 11:30:43 +02:00
parent 3e27a5ec81
commit 5fd316816d
4 changed files with 71 additions and 49 deletions

View File

@ -501,7 +501,7 @@
}); });
}); });
describe("the topic", function () { describe("topic", function () {
it("is shown the header", it("is shown the header",
mock.initConverse( mock.initConverse(
@ -511,7 +511,7 @@
await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org';
let stanza = u.toStanza(` let stanza = u.toStanza(`
<message xmlns="jabber:client" to="jc@opkode.com/_converse.js-60429116" type="groupchat" from="jdev@conference.jabber.org/ralphm"> <message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject>${text}</subject> <subject>${text}</subject>
<delay xmlns="urn:xmpp:delay" stamp="2014-02-04T09:35:39Z" from="jdev@conference.jabber.org"/> <delay xmlns="urn:xmpp:delay" stamp="2014-02-04T09:35:39Z" from="jdev@conference.jabber.org"/>
<x xmlns="jabber:x:delay" stamp="20140204T09:35:39" from="jdev@conference.jabber.org"/> <x xmlns="jabber:x:delay" stamp="20140204T09:35:39" from="jdev@conference.jabber.org"/>
@ -520,12 +520,11 @@
const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
await new Promise(resolve => view.model.once('change:subject', resolve)); await new Promise(resolve => view.model.once('change:subject', resolve));
expect(sizzle('.chat-event:last', view.el).pop().textContent.trim()).toBe('Topic set by ralphm');
const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc'));
expect(head_desc?.textContent.trim()).toBe(text); expect(head_desc?.textContent.trim()).toBe(text);
stanza = u.toStanza( stanza = u.toStanza(
`<message xmlns="jabber:client" to="jc@opkode.com/_converse.js-60429116" type="groupchat" from="jdev@conference.jabber.org/ralphm"> `<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject>This is a message subject</subject> <subject>This is a message subject</subject>
<body>This is a message</body> <body>This is a message</body>
</message>`); </message>`);
@ -536,16 +535,6 @@
expect(sizzle('.chat-msg__text').length).toBe(1); expect(sizzle('.chat-msg__text').length).toBe(1);
expect(sizzle('.chat-msg__text').pop().textContent.trim()).toBe('This is a message'); expect(sizzle('.chat-msg__text').pop().textContent.trim()).toBe('This is a message');
expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(text); expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(text);
// Removes current topic
stanza = u.toStanza(
`<message xmlns="jabber:client" to="jc@opkode.com/_converse.js-60429116" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject/>
</message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await new Promise(resolve => view.model.once('change:subject', resolve));
await u.waitUntil(() => view.el.querySelector('.chat-head__desc') === null);
expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by ralphm");
done(); done();
})); }));
@ -557,7 +546,7 @@
await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc');
const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org';
let stanza = u.toStanza(` let stanza = u.toStanza(`
<message xmlns="jabber:client" to="jc@opkode.com/_converse.js-60429116" type="groupchat" from="jdev@conference.jabber.org/ralphm"> <message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject>${text}</subject> <subject>${text}</subject>
<delay xmlns="urn:xmpp:delay" stamp="2014-02-04T09:35:39Z" from="jdev@conference.jabber.org"/> <delay xmlns="urn:xmpp:delay" stamp="2014-02-04T09:35:39Z" from="jdev@conference.jabber.org"/>
<x xmlns="jabber:x:delay" stamp="20140204T09:35:39" from="jdev@conference.jabber.org"/> <x xmlns="jabber:x:delay" stamp="20140204T09:35:39" from="jdev@conference.jabber.org"/>
@ -566,12 +555,11 @@
const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
await new Promise(resolve => view.model.once('change:subject', resolve)); await new Promise(resolve => view.model.once('change:subject', resolve));
expect(sizzle('.chat-event:last', view.el).pop().textContent.trim()).toBe('Topic set by ralphm');
const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc'));
expect(head_desc?.textContent.trim()).toBe(text); expect(head_desc?.textContent.trim()).toBe(text);
stanza = u.toStanza( stanza = u.toStanza(
`<message xmlns="jabber:client" to="jc@opkode.com/_converse.js-60429116" type="groupchat" from="jdev@conference.jabber.org/ralphm"> `<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject>This is a message subject</subject> <subject>This is a message subject</subject>
<body>This is a message</body> <body>This is a message</body>
</message>`); </message>`);
@ -592,6 +580,49 @@
expect(view.el.querySelector('.hide-topic').textContent).toBe('Show topic'); expect(view.el.querySelector('.hide-topic').textContent).toBe('Show topic');
done(); done();
})); }));
it("causes an info message to be shown when received in real-time",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
spyOn(_converse.ChatRoom.prototype, 'handleSubjectChange').and.callThrough();
await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'romeo');
const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
_converse.connection._dataRecv(test_utils.createRequest(u.toStanza(`
<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject>This is an older topic</subject>
<delay xmlns="urn:xmpp:delay" stamp="2014-02-04T09:35:39Z" from="jdev@conference.jabber.org"/>
<x xmlns="jabber:x:delay" stamp="20140204T09:35:39" from="jdev@conference.jabber.org"/>
</message>`)));
await u.waitUntil(() => view.model.handleSubjectChange.calls.count());
expect(sizzle('.chat-info__message', view.el).length).toBe(0);
const desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc'));
expect(desc.textContent.trim()).toBe('This is an older topic');
_converse.connection._dataRecv(test_utils.createRequest(u.toStanza(`
<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject>This is a new topic</subject>
</message>`)));
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 2);
const el = sizzle('.chat-info__message', view.el).pop();
expect(el.textContent.trim()).toBe('Topic set by ralphm');
await u.waitUntil(() => desc.textContent.trim() === 'This is a new topic');
// Removes current topic
const stanza = u.toStanza(
`<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="jdev@conference.jabber.org/ralphm">
<subject/>
</message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 3);
await u.waitUntil(() => view.el.querySelector('.chat-head__desc') === null);
expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by ralphm");
done();
}));
}); });

View File

@ -235,8 +235,8 @@
'text': subject, 'text': subject,
'author': 'ralphm' 'author': 'ralphm'
}}); }});
expect(sizzle('.chat-event:last').pop().textContent.trim()).toBe('Topic set by ralphm'); const text = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')?.textContent.trim());
await u.waitUntil(() => view.el.querySelector('.chat-head__desc')?.textContent.trim() === subject); expect(text).toBe(subject);
done(); done();
})); }));
}); });

View File

@ -708,7 +708,6 @@ converse.plugins.add('converse-muc-views', {
this.listenTo(this.model, 'change', debounce(() => this.renderHeading(), 250)); this.listenTo(this.model, 'change', debounce(() => this.renderHeading(), 250));
this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle); this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle);
this.listenTo(this.model, 'change:subject', this.setChatRoomSubject);
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm); this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
this.listenTo(this.model, 'destroy', this.hide); this.listenTo(this.model, 'destroy', this.hide);
this.listenTo(this.model, 'show', this.show); this.listenTo(this.model, 'show', this.show);
@ -2004,26 +2003,6 @@ converse.plugins.add('converse-muc-views', {
this.renderAfterTransition(); this.renderAfterTransition();
} }
return this; return this;
},
setChatRoomSubject () {
const subject = this.model.get('subject');
if (!subject.text && !subject.author) {
return; // Probably a new MUC
}
const author = subject.author;
// For translators: the %1$s part will get
// replaced by the user's name.
// Example: Topic set by JC Brand
const message = subject.text ? __('Topic set by %1$s', author) : __('Topic cleared by %1$s', author);
this.msgs_container.insertAdjacentHTML(
'beforeend',
tpl_info({
'isodate': (new Date()).toISOString(),
'extra_classes': 'chat-event',
'message': message
}));
this.scrollDown();
} }
}); });

View File

@ -1682,22 +1682,34 @@ converse.plugins.add('converse-muc', {
}, },
/** /**
* Handle a subject change and return `true` if so. * Handle a possible subject change and return `true` if so.
* @private * @private
* @method _converse.ChatRoom#subjectChangeHandled * @method _converse.ChatRoom#handleSubjectChange
* @param { object } attrs - The message attributes * @param { object } attrs - Attributes representing a received
* message, as returned by {@link stanza_utils.getMessageAttributesFromStanza}
*/ */
subjectChangeHandled (attrs) { handleSubjectChange (attrs) {
if (isString(attrs.subject) && !attrs.thread && !attrs.message) { if (isString(attrs.subject) && !attrs.thread && !attrs.message) {
// https://xmpp.org/extensions/xep-0045.html#subject-mod // https://xmpp.org/extensions/xep-0045.html#subject-mod
// ----------------------------------------------------- // -----------------------------------------------------
// The subject is changed by sending a message of type "groupchat" to the <room@service>, // The subject is changed by sending a message of type "groupchat" to the <room@service>,
// where the <message/> MUST contain a <subject/> element that specifies the new subject but // where the <message/> MUST contain a <subject/> element that specifies the new subject but
// MUST NOT contain a <body/> element (or a <thread/> element). // MUST NOT contain a <body/> element (or a <thread/> element).
const subject = attrs.subject;
const author = attrs.nick;
u.safeSave(this, { u.safeSave(this, {
'subject': {'author': attrs.nick, 'text': attrs.subject || ''}, 'subject': {author, 'text': attrs.subject || ''},
'subject_hidden': attrs.subject ? false : this.get('subject_hidden') 'subject_hidden': subject ? false : this.get('subject_hidden')
}); });
if (!attrs.is_delayed) {
const message = subject ? __('Topic set by %1$s', author) : __('Topic cleared by %1$s', author);
const data = {
message,
'nick': attrs.nick,
'type': 'info'
};
this.createMessage(data);
}
return true; return true;
} }
return false; return false;
@ -1998,7 +2010,7 @@ converse.plugins.add('converse-muc', {
if (await this.handleRetraction(attrs) || if (await this.handleRetraction(attrs) ||
await this.handleModeration(attrs) || await this.handleModeration(attrs) ||
this.subjectChangeHandled(attrs) || this.handleSubjectChange(attrs) ||
this.ignorableCSN(attrs)) { this.ignorableCSN(attrs)) {
return api.trigger('message', {'stanza': original_stanza}); return api.trigger('message', {'stanza': original_stanza});
} }
@ -2070,10 +2082,10 @@ converse.plugins.add('converse-muc', {
/** /**
* Create info messages based on a received presence stanza * Create info messages based on a received presence or message stanza
* @private * @private
* @method _converse.ChatRoom#createInfoMessages * @method _converse.ChatRoom#createInfoMessages
* @param { XMLElement } stanza: The presence stanza received * @param { XMLElement } stanza
*/ */
createInfoMessages (stanza) { createInfoMessages (stanza) {
const is_self = stanza.querySelector("status[code='110']") !== null; const is_self = stanza.querySelector("status[code='110']") !== null;