Skip to content

Commit

Permalink
Add support for XEP-0393 message styling
Browse files Browse the repository at this point in the history
Fixes #1083

Directives are rendered as templates and their bodies are MessageText instances.
We thereby achieve the necessary nesting of directives (and other rich
elements inside directives) by letting each directive
body render itself similarly to how the whole message body is rendered.
  • Loading branch information
jcbrand committed Nov 24, 2020
1 parent 6b85c2e commit 0217e8a
Show file tree
Hide file tree
Showing 16 changed files with 721 additions and 210 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
],
"lines-around-comment": "off",
"lines-around-directive": "off",
"max-depth": "error",
"max-depth": "off",
"max-len": "off",
"max-lines": "off",
"max-nested-callbacks": "off",
Expand Down Expand Up @@ -192,7 +192,7 @@
"no-undef-init": "error",
"no-undefined": "off",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unmodified-loop-condition": "off",
"no-unneeded-ternary": "off",
"no-unused-vars": "error",
"no-unused-expressions": "off",
Expand Down
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
configuration settings should now be accessed via `_converse.api.settings.get` and not directly on the `_converse` object.
Soon we'll deprecate the latter, so prepare now.

- #515 Add support for XEP-0050 Ad-Hoc commands
- #1083 Add support for XEP-0393 Message Styling
- #2231: add sort_by_query and remove sort_by_length
- #1313: Stylistic improvements to the send button
- #1481: MUC OMEMO: Error No record for device
Expand All @@ -28,7 +30,6 @@ Soon we'll deprecate the latter, so prepare now.
- #1793: Send button doesn't appear in Firefox in 1:1 chats
- #1820: Set focus on jid field after controlbox is loaded
- #1822: Don't log error if user has no bookmarks
- #515 Add support for XEP-0050 Ad-Hoc commands
- #1823: New config options [muc_roomid_policy](https://conversejs.org/docs/html/configuration.html#muc-roomid-policy)
and [muc_roomid_policy_hint](https://conversejs.org/docs/html/configuration.html#muc-roomid-policy-hint)
- #1826: A user can now add himself as a contact
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ In embedded mode, Converse can be embedded into an element in the DOM.
- [XEP-0372](https://xmpp.org/extensions/xep-0372.html) References
- [XEP-0382](https://xmpp.org/extensions/xep-0382.html) Spoiler messages
- [XEP-0384](https://xmpp.org/extensions/xep-0384.html) OMEMO Encryption
- [XEP-0393](https://xmpp.org/extensions/xep-0393.html) Message styling
- [XEP-0422](https://xmpp.org/extensions/xep-0422.html) Message Fastening (limited support)
- [XEP-0424](https://xmpp.org/extensions/xep-0424.html) Message Retractions
- [XEP-0425](https://xmpp.org/extensions/xep-0425.html) Message Moderation
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ <h2>Features</h2>
<li>Hidden messages (aka Spoilers) (<a href="https://xmpp.org/extensions/xep-0382.html" target="_blank" rel="noopener">XEP 382</a>)</li>
<li>Client state indication (<a href="https://xmpp.org/extensions/xep-0352.html" target="_blank" rel="noopener">XEP 352</a>)</li>
<li>OMEMO encrypted messaging (<a href="https://xmpp.org/extensions/xep-0384.html" target="_blank" rel="noopener">XEP 384</a>)</li>
<li>Message Styling (<a href="https://xmpp.org/extensions/xep-0384.html" target="_blank" rel="noopener">XEP 393</a>)</li>
<li>Anonymous logins, see the <a href="/demo/anonymous.html" target="_blank" rel="noopener">anonymous login demo</a></li>
<li>Message corrections, retractions and moderation</li>
<li>Translated into over 30 languages</li>
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module.exports = function(config) {
{ pattern: "spec/user-details-modal.js", type: 'module' },
{ pattern: "spec/messages.js", type: 'module' },
{ pattern: "spec/corrections.js", type: 'module' },
{ pattern: "spec/message-styling.js", type: 'module' },
{ pattern: "spec/receipts.js", type: 'module' },
{ pattern: "spec/muc_messages.js", type: 'module' },
{ pattern: "spec/me-messages.js", type: 'module' },
Expand Down
15 changes: 15 additions & 0 deletions sass/_messages.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
#conversejs {
.styling-directive {
color: var(--subdued-color);
}

.older-msg {
time {
font-weight: bold;
}
}
.message {
blockquote {
margin-left: 0.5em;
margin-bottom: 0.25em;
padding-right: 1em;
color: var(--subdued-color);
border-left: 0.3em solid var(--subdued-color);
padding-left: 0.5em;
}
code {
font-family: monospace;
}
.mention {
font-weight: bold;
}
Expand Down
2 changes: 1 addition & 1 deletion spec/me-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { u, sizzle, $msg } = converse.env;

describe("A Groupchat Message", function () {

fit("supports the /me command",
it("supports the /me command",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
Expand Down
4 changes: 1 addition & 3 deletions spec/mentions.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ describe("An incoming groupchat message", function () {
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
expect(message.classList.length).toEqual(1);
expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
'&gt;hello <span class="mention">z3r0</span> '+
'<span class="mention mention--self badge badge-info">tom</span> '+
'<span class="mention">mr.robot</span>, how are you?');
'<blockquote>hello <span class="mention">z3r0</span> <span class="mention mention--self badge badge-info">tom</span> <span class="mention">mr.robot</span>, how are you?</blockquote>');
done();
}));
});
Expand Down
257 changes: 257 additions & 0 deletions spec/message-styling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/*global mock, converse */

const { u, Promise, $msg } = converse.env;

fdescribe("A incoming chat Message", function () {

it("can be styled with XEP-0393 message styling hints",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
function (done) {

done();
}));

it("can have styling disabled",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {

const include_nick = false;
await mock.waitForRoster(_converse, 'current', 2, include_nick);
await mock.openControlBox(_converse);

const msgtext = '> _ >';
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msg = $msg({
'from': sender_jid,
'id': u.getUniqueId(),
'to': _converse.connection.jid,
'type': 'chat',
'xmlns': 'jabber:client'
}).c('body').t(msgtext).tree();

await _converse.handleMessageStanza(msg);
done();
}));

it("can be styled with span XEP-0393 message styling hints",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {

let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);

msg_text = "This *message _contains_* styling hints! \`Here's *some* code\`";
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = view.el.querySelector('converse-chat-message-body');
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'This <span class="styling-directive">*</span>'+
'<b>message <span class="styling-directive">_</span><i>contains</i><span class="styling-directive">_</span></b>'+
'<span class="styling-directive">*</span>'+
' styling hints! '+
'<span class="styling-directive">`</span><code>Here\'s *some* code</code><span class="styling-directive">`</span>'
);

msg_text = "Here's a ~strikethrough section~";
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'Here\'s a <span class="styling-directive">~</span><del>strikethrough section</del><span class="styling-directive">~</span>');

// Span directives containing hyperlinks
msg_text = "~Check out this site: https://conversejs.org~"
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'<span class="styling-directive">~</span>'+
'<del>Check out this site: <a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a></del>'+
'<span class="styling-directive">~</span>');

// Images inside directives aren't shown inline
const base_url = 'https://conversejs.org';
msg_text = `*${base_url}/logo/conversejs-filled.svg*`;
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'<span class="styling-directive">*</span>'+
'<b><a target="_blank" rel="noopener" href="https://conversejs.org/logo/conversejs-filled.svg">https://conversejs.org/logo/conversejs-filled.svg</a></b>'+
'<span class="styling-directive">*</span>');

// Emojis inside directives
msg_text = `~ Hello! :poop: ~`;
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'<span class="styling-directive">~</span><del> Hello! <span title=":poop:">💩</span> </del><span class="styling-directive">~</span>');

// Span directives don't cross lines
msg_text = "This *is not a styling hint \n * _But this is_!";
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'This *is not a styling hint \n'+
' * <span class="styling-directive">_</span><i>But this is</i><span class="styling-directive">_</span>!');

msg_text = `(There are three blocks in this body marked by parens,)\n (but there is no *formatting)\n (as spans* may not escape blocks.)`;
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') === msg_text);

done();
}));

it("can be styled with block XEP-0393 message styling hints",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {

let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);

msg_text = `Here's a code block: \n\`\`\`\nInside the code-block, <code>hello</code> we don't enable *styling hints* like ~these~\n\`\`\``;
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'Here\'s a code block: \n'+
'<div class="styling-directive">```</div><code class="block">Inside the code-block, &lt;code&gt;hello&lt;/code&gt; we don\'t enable *styling hints* like ~these~\n'+
'</code><div class="styling-directive">```</div>'
);

msg_text = "```\nignored\n(println \"Hello, world!\")\n```\nThis should show up as monospace, preformatted text ^";
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'<div class="styling-directive">```</div>'+
'<code class="block">ignored\n(println "Hello, world!")\n</code>'+
'<div class="styling-directive">```</div>\n'+
'This should show up as monospace, preformatted text ^');


msg_text = "```ignored\n (println \"Hello, world!\")\n ```\n\n This should not show up as monospace, *preformatted* text ^";
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'```ignored\n (println "Hello, world!")\n ```\n\n'+
' This should not show up as monospace, '+
'<span class="styling-directive">*</span><b>preformatted</b><span class="styling-directive">*</span> text ^');
done();
}));

it("can be styled with quote XEP-0393 message styling hints",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {

let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);

msg_text = `> This is quoted text\n>This is also quoted\nThis is not quoted`;
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'<blockquote> This is quoted text\nThis is also quoted</blockquote>\nThis is not quoted');

msg_text = `> This is *quoted* text\n>This is \`also _quoted_\`\nThis is not quoted`;
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'<blockquote> This is <span class="styling-directive">*</span><b>quoted</b><span class="styling-directive">*</span> text\n'+
'This is <span class="styling-directive">`</span><code>also _quoted_</code><span class="styling-directive">`</span></blockquote>\n'+
'This is not quoted');

msg_text = `> > This is doubly quoted text`;
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') === "<blockquote> <blockquote> This is doubly quoted text</blockquote></blockquote>");

msg_text = ">```\n>ignored\n> <span></span> (println \"Hello, world!\")\n>```\n> This should show up as monospace, preformatted text ^";
msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'<blockquote>'+
'<div class="styling-directive">```</div>'+
'<code class="block">ignored\n &lt;span&gt;&lt;/span&gt; (println "Hello, world!")\n'+
'</code><div class="styling-directive">```</div>\n'+
' This should show up as monospace, preformatted text ^'+
'</blockquote>');
done();
}));
});


describe("A outgoing groupchat Message", function () {

it("can be styled with span XEP-0393 message styling hints that contain mentions",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {

const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.api.chatviews.get(muc_jid);
const msg_text = "This *message mentions romeo*";
const msg = $msg({
from: 'lounge@montague.lit/gibson',
id: u.getUniqueId(),
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(msg_text).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'23', 'end':'29', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).nodeTree;
await view.model.handleMessageStanza(msg);
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
expect(message.classList.length).toEqual(1);

const msg_el = Array.from(view.el.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!---->/g, '') ===
'This <span class="styling-directive">*</span><b>message mentions <span class="mention mention--self badge badge-info">romeo</span></b><span class="styling-directive">*</span>');
done();
}));
});
Loading

0 comments on commit 0217e8a

Please sign in to comment.