diff --git a/.eslintrc.json b/.eslintrc.json
index c048b97b0f..890eb37b4c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -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",
@@ -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",
diff --git a/CHANGES.md b/CHANGES.md
index b4aec3e0a6..4d9bd0f975 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -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
@@ -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
diff --git a/README.md b/README.md
index d4a9dc8a44..36e29afb3c 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/index.html b/index.html
index f4a73102a6..53e71404f6 100644
--- a/index.html
+++ b/index.html
@@ -197,6 +197,7 @@
Features
Hidden messages (aka Spoilers) (XEP 382)
Client state indication (XEP 352)
OMEMO encrypted messaging (XEP 384)
+ Message Styling (XEP 393)
Anonymous logins, see the anonymous login demo
Message corrections, retractions and moderation
Translated into over 30 languages
diff --git a/karma.conf.js b/karma.conf.js
index e8dd9c9ca2..e9477f7f0e 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -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' },
diff --git a/sass/_messages.scss b/sass/_messages.scss
index 7607f813d2..9003b17347 100644
--- a/sass/_messages.scss
+++ b/sass/_messages.scss
@@ -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;
}
diff --git a/spec/me-messages.js b/spec/me-messages.js
index 937b5b53c1..f9fe7fde95 100644
--- a/spec/me-messages.js
+++ b/spec/me-messages.js
@@ -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) {
diff --git a/spec/mentions.js b/spec/mentions.js
index 6948b1bdf8..a88a8e9a18 100644
--- a/spec/mentions.js
+++ b/spec/mentions.js
@@ -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(
- '>hello z3r0 '+
- 'tom '+
- 'mr.robot, how are you?');
+ 'hello z3r0 tom mr.robot, how are you?
');
done();
}));
});
diff --git a/spec/message-styling.js b/spec/message-styling.js
new file mode 100644
index 0000000000..19680331fc
--- /dev/null
+++ b/spec/message-styling.js
@@ -0,0 +1,283 @@
+/*global mock, converse */
+
+const { u, Promise, $msg } = converse.env;
+
+fdescribe("A incoming chat Message", function () {
+
+ 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 msg_text = '> _ >';
+ 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(msg_text).up()
+ .c('unstyled', {'xmlns': 'urn:xmpp:styling:0'}).tree();
+ await _converse.handleMessageStanza(msg);
+
+ const view = _converse.api.chatviews.get(sender_jid);
+ await u.waitUntil(() => view.model.messages.length);
+ expect(view.model.messages.models[0].get('is_unstyled')).toBe(true);
+
+ setTimeout(() => {
+ const msg_el = view.el.querySelector('converse-chat-message-body');
+ expect(msg_el.innerText).toBe(msg_text);
+ done();
+ }, 500);
+ }));
+
+ 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 *'+
+ 'message _contains_'+
+ '*'+
+ ' styling hints! '+
+ '`Here\'s *some* code
`'
+ );
+
+ 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 ~strikethrough section~');
+
+ // 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, '') ===
+ '~'+
+ 'Check out this site: https://conversejs.org'+
+ '~');
+
+ // 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, '') ===
+ '*'+
+ 'https://conversejs.org/logo/conversejs-filled.svg'+
+ '*');
+
+ // 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, '') ===
+ '~ Hello! 💩 ~');
+
+ // 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'+
+ ' * _But this is_!');
+
+ 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.)\n ~strikethrough~`;
+ 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, '') ===
+ '(There are three blocks in this body marked by parens,)\n'+
+ ' (but there is no *formatting)\n'+
+ ' (as spans* may not escape blocks.)\n'+
+ ' ~strikethrough~');
+
+ // Some edge-case (unspecified) spans
+ msg_text = `__ hello world _`;
+ 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, '') ===
+ '__ hello world _');
+
+ 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, hello
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'+
+ '```
Inside the code-block, <code>hello</code> we don\'t enable *styling hints* like ~these~\n'+
+ '
```
'
+ );
+
+ 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, '') ===
+ '```
'+
+ 'ignored\n(println "Hello, world!")\n
'+
+ '```
\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, '+
+ '*preformatted* 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, '') ===
+ ' This is quoted text\nThis is also quoted
\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, '') ===
+ ' This is *quoted* text\n'+
+ 'This is `also _quoted_
`
\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, '') === " This is doubly quoted text
");
+
+ msg_text = ">```\n>ignored\n> (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, '') ===
+ ''+
+ '```
'+
+ 'ignored\n <span></span> (println "Hello, world!")\n'+
+ '
```
\n'+
+ ' This should show up as monospace, preformatted text ^'+
+ '
');
+
+ msg_text = '> ```\n> (println "Hello, world!")\n\nThe entire blockquote is a preformatted text block, but this line is plaintext!';
+ 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, '') ===
+ ' ```\n (println "Hello, world!")
\n\n'+
+ 'The entire blockquote is a preformatted text block, but this line is plaintext!');
+ 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 *message mentions romeo*');
+ done();
+ }));
+});
diff --git a/spec/messages.js b/spec/messages.js
index 59b3d9f811..a781c1e3a9 100644
--- a/spec/messages.js
+++ b/spec/messages.js
@@ -569,7 +569,6 @@ describe("A Chat Message", function () {
await u.waitUntil(() => msg.innerHTML.replace(//g, '') ===
'This message contains a hyperlink with forbidden query params: https://www.opkode.com/?id=0');
-
// Test assigning a string to filter_url_query_params
_converse.api.settings.set('filter_url_query_params', 'utm_medium');
message = 'Another message with a hyperlink with forbidden query params: https://www.opkode.com/?id=0&utm_content=1&utm_medium=2&s=1';
diff --git a/spec/xss.js b/spec/xss.js
index a72e465a96..dfdaf31907 100644
--- a/spec/xss.js
+++ b/spec/xss.js
@@ -1,4 +1,4 @@
-/*global mock */
+/*global mock, converse */
const $pres = converse.env.$pres;
const sizzle = converse.env.sizzle;
@@ -145,13 +145,11 @@ describe("XSS", function () {
expect(msg.textContent).toEqual(message);
expect(msg.innerHTML.replace(//g, ''))
.toEqual('http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever');
-
await u.waitUntil(() => msg.innerHTML.replace(//g, '') ===
'http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever');
message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever';
await mock.sendMessage(view, message);
-
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(//g, '') ===
@@ -159,14 +157,12 @@ describe("XSS", function () {
message = "https://en.wikipedia.org/wiki/Ender's_Game";
await mock.sendMessage(view, message);
-
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(//g, '') === ''+message+'');
message = "";
await mock.sendMessage(view, message);
-
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(//g, '') ===
@@ -174,7 +170,6 @@ describe("XSS", function () {
message = '';
await mock.sendMessage(view, message);
-
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(//g, '') ===
@@ -182,7 +177,6 @@ describe("XSS", function () {
message = `https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2`
await mock.sendMessage(view, message);
-
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(//g, '') ===
diff --git a/src/headless/converse-core.js b/src/headless/converse-core.js
index c276f0a7ac..3dac490358 100644
--- a/src/headless/converse-core.js
+++ b/src/headless/converse-core.js
@@ -52,6 +52,7 @@ Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
Strophe.addNamespace('SID', 'urn:xmpp:sid:0');
Strophe.addNamespace('SPOILER', 'urn:xmpp:spoiler:0');
Strophe.addNamespace('STANZAS', 'urn:ietf:params:xml:ns:xmpp-stanzas');
+Strophe.addNamespace('STYLING', 'urn:xmpp:styling:0');
Strophe.addNamespace('VCARD', 'vcard-temp');
Strophe.addNamespace('VCARDUPDATE', 'vcard-temp:x:update');
Strophe.addNamespace('XFORM', 'jabber:x:data');
diff --git a/src/headless/utils/parse-helpers.js b/src/headless/utils/parse-helpers.js
index 8036f3e537..a0765fa28e 100644
--- a/src/headless/utils/parse-helpers.js
+++ b/src/headless/utils/parse-helpers.js
@@ -1,7 +1,7 @@
/**
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
- * @description Pure functions to help funcitonally parse messages.
+ * @description Pure functions to help functionally parse messages.
* @todo Other parsing helpers can be made more abstract and placed here.
*/
const helpers = {};
diff --git a/src/headless/utils/stanza.js b/src/headless/utils/stanza.js
index fa642126a6..374e6c107f 100644
--- a/src/headless/utils/stanza.js
+++ b/src/headless/utils/stanza.js
@@ -441,9 +441,10 @@ const st = {
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
- * @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
+ * @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
+ * @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Object } encrypted - XEP-0384 encryption payload attributes
* @property { String } body - The contents of the tag of the message stanza
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
@@ -489,6 +490,7 @@ const st = {
'is_delayed': !!delay,
'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
'is_marker': !!marker,
+ 'is_unstyled': !!sizzle(`unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length,
'marker_id': marker && marker.getAttribute('id'),
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
'nick': contact?.attributes?.nickname,
@@ -581,9 +583,10 @@ const st = {
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
- * @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
+ * @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
+ * @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Object } encrypted - XEP-0384 encryption payload attributes
* @property { String } body - The contents of the tag of the message stanza
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
@@ -632,6 +635,7 @@ const st = {
'is_headline': st.isHeadline(stanza),
'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
'is_marker': !!marker,
+ 'is_unstyled': !!sizzle(`unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length,
'marker_id': marker && marker.getAttribute('id'),
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
'receipt_id': getReceiptId(stanza),
diff --git a/src/shared/message/styling.js b/src/shared/message/styling.js
new file mode 100644
index 0000000000..8a84b9b19d
--- /dev/null
+++ b/src/shared/message/styling.js
@@ -0,0 +1,146 @@
+/**
+ * @copyright 2020, the Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ * @description Utility functions to help with parsing XEP-393 message styling hints
+ * @todo Other parsing helpers can be made more abstract and placed here.
+ */
+import { html } from 'lit-element';
+import { renderStylingDirectiveBody } from '../../templates/directives/styling.js';
+
+
+const styling_directives = ['*', '_', '~', '`', '```', '>'];
+const styling_map = {
+ '*': {'name': 'strong', 'type': 'span'},
+ '_': {'name': 'emphasis', 'type': 'span'},
+ '~': {'name': 'strike', 'type': 'span'},
+ '`': {'name': 'preformatted', 'type': 'span'},
+ '```': {'name': 'preformatted_block', 'type': 'block'},
+ '>': {'name': 'quote', 'type': 'block'}
+};
+
+const dont_escape = ['_', '>', '`', '~'];
+
+const styling_templates = {
+ // m is the chatbox model
+ // i is the offset of this directive relative to the start of the original message
+ 'emphasis': (txt, m, i) => html`_${renderStylingDirectiveBody(txt, m, i)}_`,
+ 'preformatted': txt => html`\`${txt}
\``,
+ 'preformatted_block': txt => html`\`\`\`
${txt}
\`\`\`
`,
+ 'quote': (txt, m, i) => html`${renderStylingDirectiveBody(txt, m, i)}
`,
+ 'strike': (txt, m, i) => html`~${renderStylingDirectiveBody(txt, m, i)}~`,
+ 'strong': (txt, m, i) => html`*${renderStylingDirectiveBody(txt, m, i)}*`,
+};
+
+
+/**
+ * Checks whether a given character "d" at index "i" of "text" is a valid
+ * opening or closing directive.
+ * @param { String } d - The potential directive
+ * @param { String } text - The text in which the directive appears
+ * @param { Number } i - The directive index
+ * @param { Boolean } opening - Check for a valid opening or closing directive
+ */
+function isValidDirective (d, text, i, opening) {
+ // Ignore directives that are parts of words
+ // More info on the Regexes used here: https://javascript.info/regexp-unicode#unicode-properties-p
+ if (opening) {
+ const regex = RegExp(dont_escape.includes(d) ? `(\\p{L}|\\p{N})${d}` : `(\\p{L}|\\p{N})\\${d}`, 'u');
+ if (i > 1 && regex.test(text.slice(i-1, i+d.length))) {
+ return false;
+ }
+ } else {
+ const regex = RegExp(dont_escape.includes(d) ? `${d}(\\p{L}|\\p{N})` : `\\${d}(\\p{L}|\\p{N})`, 'u');
+ if (i < text.length-1 && regex.test(text.slice(i-d.length, i+1))) {
+ return false;
+ }
+ }
+ if (opening && styling_map[d].type === 'span' && !text.slice(i+1).split('\n').shift().includes(d)) {
+ // span directive without closing part before end or line-break, so not valid
+ return false;
+ }
+ return true;
+}
+
+
+/**
+ * Given a specific index "i" of "text", return the directive it matches or
+ * null otherwise.
+ * @param { String } text - The text in which the directive appears
+ * @param { Number } i - The directive index
+ * @param { Boolean } opening - Whether we're looking for an opening or closing directive
+ */
+function getDirective (text, i, opening=true) {
+ // TODO: blockquote is only valid if on own line
+ // TODO: blockquote without end quote is valid until end of text or of containing quote
+ let d;
+ if (text.slice(i).match(/(^```\s*\n|^```\s*$)/) && (i === 0 || text[i-1] === '\n' || text[i-1] === '>')) {
+ d = text.slice(i, i+3);
+ } else if (styling_directives.includes(text.slice(i, i+1)) && text[i] !== text[i+1]) {
+ d = text.slice(i, i+1);
+ if (!isValidDirective(d, text, i, opening)) return null;
+ } else {
+ return null;
+ }
+ return d;
+}
+
+
+/**
+ * Given an opening directive "d", an index "i" and the text, check whether
+ * we've found the closing directive.
+ * @param { String } d -The directive
+ * @param { Number } i - The directive index
+ * @param { String } text -The text in which the directive appears
+ */
+function isDirectiveEnd (d, i, text) {
+ const dtype = styling_map[d].type; // directive type
+ return i === text.length || getDirective(text, i, false) === d || (dtype === 'span' && text[i] === '\n');
+}
+
+
+function getDirectiveLength (d, text, i) {
+ if (!d) { return 0; }
+ const begin = i;
+ i += d.length;
+ if (isQuoteDirective(d)) {
+ i += text.slice(i).split(/\n[^>]/).shift().length;
+ return i-begin;
+ } else {
+ // Set i to the last char just before the end of the direcive
+ while (!isDirectiveEnd(d, i, text)) { i++; }
+ if (i <= text.length) {
+ i += d.length;
+ return i-begin;
+ }
+ }
+ return 0;
+}
+
+
+export function getDirectiveAndLength (text, i) {
+ const d = getDirective(text, i);
+ const length = d ? getDirectiveLength(d, text, i) : 0;
+ return { d, length };
+}
+
+
+export const isQuoteDirective = (d) => ['>', '>'].includes(d);
+
+
+export function getDirectiveTemplate (d, text, model, offset) {
+ const template = styling_templates[styling_map[d].name];
+ if (isQuoteDirective(d)) {
+ return template(text.replace(/\n>/g, '\n'), model, offset);
+ } else {
+ return template(text, model, offset);
+ }
+}
+
+
+export function containsDirectives (text) {
+ for (let i=0; i typeof s === 'string';
+
+const tpl_mention_with_nick = (o) => html`${o.mention}`;
+const tpl_mention = (o) => html`${o.mention}`;
+
+
+/**
+ * @class MessageText
+ * A String subclass that is used to represent the rich text
+ * of a chat message.
+ *
+ * The "rich" parts of the text is represented by lit-html TemplateResult
+ * objects which are added via the {@link MessageText.addTemplateResult}
+ * method and saved as metadata.
+ *
+ * By default Converse adds TemplateResults to support emojis, hyperlinks,
+ * images, map URIs and mentions.
+ *
+ * 3rd party plugins can listen for the `beforeMessageBodyTransformed`
+ * and/or `afterMessageBodyTransformed` events and then call
+ * `addTemplateResult` on the MessageText instance in order to add their own
+ * rich features.
+ */
+export class MessageText extends String {
+
+ /**
+ * Create a new {@link MessageText} instance.
+ * @param { String } text - The plain text that was received from the `` stanza.
+ * @param { Message } model
+ * @param { Integer } offset - The offset of this particular piece of text
+ * from the start of the original message text. This is necessary because
+ * MessageText instances can be nested when templates call directives
+ * which create new MessageText instances (as happens with XEP-393 styling directives).
+ * @param { Boolean } show_images - Whether image URLs should be rendered as tags.
+ * @param { Function } onImgLoad
+ * @param { Function } onImgClick
+ */
+ constructor (text, model, offset=0, show_images, onImgLoad, onImgClick) {
+ super(text);
+ this.model = model;
+ this.offset = offset;
+ this.onImgClick = onImgClick;
+ this.onImgLoad = onImgLoad;
+ this.references = [];
+ this.show_images = show_images;
+ this.payload = [];
+ }
+
+ addHyperlinks (text) {
+ const objs = [];
+ try {
+ const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
+ URI.withinString(text, (url, start, end) => {
+ objs.push({url, start, end})
+ return url;
+ } , parse_options);
+ } catch (error) {
+ log.debug(error);
+ return;
+ }
+ objs.forEach(url_obj => {
+ const url_text = text.slice(url_obj.start, url_obj.end);
+ const filtered_url = u.filterQueryParamsFromURL(url_text);
+ this.addTemplateResult(
+ url_obj.start,
+ url_obj.end,
+ this.show_images && u.isImageURL(url_text) && u.isImageDomainAllowed(url_text) ?
+ u.convertToImageTag(filtered_url, this.onImgLoad, this.onImgClick) :
+ u.convertUrlToHyperlink(filtered_url),
+ );
+ });
+ }
+
+ addMapURLs (text) {
+ const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
+ const matches = text.matchAll(regex);
+ for (const m of matches) {
+ this.addTemplateResult(
+ m.index,
+ m.index+m.input.length,
+ u.convertUrlToHyperlink(m.input.replace(regex, _converse.geouri_replacement))
+ );
+ }
+ }
+
+ async addEmojis (text) {
+ await api.emojis.initialize();
+ const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())];
+ references.forEach(e => {
+ this.addTemplateResult(
+ e.begin,
+ e.end,
+ getEmojiMarkup(e, {'add_title_wrapper': true})
+ );
+ });
+ }
+
+ addMentionReferences (text, offset) {
+ if (!this.model.collection) {
+ // This model doesn't belong to a collection anymore, so it must be
+ // have been removed in the meantime and can be ignored.
+ log.debug('addMentionReferences: ignoring dangling model');
+ return;
+ }
+ const nick = this.model.collection.chatbox.get('nick');
+ this.model.get('references')?.forEach(ref => {
+ const begin = Number(ref.begin)-offset;
+ if (begin >= text.length) {
+ return;
+ }
+ const end = Number(ref.end)-offset;
+ const mention = text.slice(begin, end);
+ if (mention === nick) {
+ this.addTemplateResult(begin, end, tpl_mention_with_nick({mention}));
+ } else {
+ this.addTemplateResult(begin, end, tpl_mention({mention}));
+ }
+ });
+ }
+
+ addStylingReferences () {
+ if (this.model.get('is_unstyled')) {
+ return;
+ }
+ let i = 0;
+ const references = [];
+ if (containsDirectives(this)) {
+ while (i < this.length) {
+ const { d, length } = getDirectiveAndLength(this, i);
+ if (d && length) {
+ const begin = d === '```' ? i+d.length+1 : i+d.length;
+ const end = i+length;
+ const slice_end = isQuoteDirective(d) ? end : end-d.length;
+ references.push({
+ 'begin': i,
+ 'template': getDirectiveTemplate(d, this.slice(begin, slice_end), this.model, i+d.length),
+ end,
+ });
+ i = end;
+ }
+ i++;
+ }
+ }
+ references.forEach(ref => this.addTemplateResult(ref.begin, ref.end, ref.template));
+ }
+
+ trimMeMessage () {
+ if (this.offset === 0) {
+ // Subtract `/me ` from 3rd person messages
+ if (this.isMeCommand()) {
+ this.payload[0] = this.payload[0].substring(4);
+ }
+ }
+ }
+
+ /**
+ * Parse the text and add template references for rendering the "rich" parts.
+ *
+ * @param { MessageText } text
+ * @param { Boolean } show_images - Should URLs of images be rendered as `` tags?
+ * @param { Function } onImgLoad
+ * @param { Function } onImgClick
+ **/
+ async addTemplates() {
+ /**
+ * Synchronous event which provides a hook for transforming a chat message's body text
+ * before the default transformations have been applied.
+ * @event _converse#beforeMessageBodyTransformed
+ * @param { _converse.Message } model - The model representing the message
+ * @param { MessageText } text - A {@link MessageText } instance. You
+ * can call {@link MessageText#addTemplateResult } on it in order to
+ * add TemplateResult objects meant to render rich parts of the
+ * message.
+ * @example _converse.api.listen.on('beforeMessageBodyTransformed', (view, text) => { ... });
+ */
+ await api.trigger('beforeMessageBodyTransformed', this, {'Synchronous': true});
+
+ this.addStylingReferences();
+ const payload = this.marshall();
+
+ let offset = this.offset;
+ for (const text of payload) {
+ if (isString(text)) {
+ this.addHyperlinks(text);
+ this.addMapURLs(text);
+ await this.addEmojis(text);
+ this.addMentionReferences(text, offset);
+ offset += text.length;
+ } else {
+ offset += text.begin;
+ }
+ }
+
+ /**
+ * Synchronous event which provides a hook for transforming a chat message's body text
+ * after the default transformations have been applied.
+ * @event _converse#afterMessageBodyTransformed
+ * @param { _converse.Message } model - The model representing the message
+ * @param { MessageText } text - A {@link MessageText } instance. You
+ * can call {@link MessageText#addTemplateResult} on it in order to
+ * add TemplateResult objects meant to render rich parts of the
+ * message.
+ * @example _converse.api.listen.on('afterMessageBodyTransformed', (view, text) => { ... });
+ */
+ await api.trigger('afterMessageBodyTransformed', this, {'Synchronous': true});
+
+ this.payload = this.marshall();
+ this.trimMeMessage();
+ this.payload = this.payload.map(item => isString(item) ? item : item.template);
+ }
+
+ /**
+ * The "rich" markup parts of a chat message are represented by lit-html
+ * TemplateResult objects.
+ *
+ * This method can be used to add new template results to this message's
+ * text.
+ *
+ * @method MessageText.addTemplateResult
+ * @param { Number } begin - The starting index of the plain message text
+ * which is being replaced with markup.
+ * @param { Number } end - The ending index of the plain message text
+ * which is being replaced with markup.
+ * @param { Object } template - The lit-html TemplateResult instance
+ */
+ addTemplateResult (begin, end, template) {
+ this.references.push({begin, end, template});
+ }
+
+ isMeCommand () {
+ const text = this.toString();
+ if (!text) {
+ return false;
+ }
+ return text.startsWith('/me ');
+ }
+
+ static replaceText (text) {
+ return convertASCII2Emoji(text.replace(/\n\n+/g, '\n\n'));
+ }
+
+ marshall () {
+ let list = [this.toString()];
+ this.references
+ .sort((a, b) => b.begin - a.begin)
+ .forEach(ref => {
+ const text = list.shift();
+ list = [
+ text.slice(0, ref.begin),
+ ref,
+ text.slice(ref.end),
+ ...list
+ ];
+ });
+ return list.reduce((acc, i) => isString(i) ? [...acc, MessageText.replaceText(i)] : [...acc, i], []);
+ }
+}
diff --git a/src/templates/directives/body.js b/src/templates/directives/body.js
index 8a117ec843..4b18762e37 100644
--- a/src/templates/directives/body.js
+++ b/src/templates/directives/body.js
@@ -1,168 +1,10 @@
-import URI from "urijs";
-import log from '@converse/headless/log';
-import { _converse, api, converse } from "@converse/headless/converse-core";
-import { convertASCII2Emoji, getEmojiMarkup, getCodePointReferences, getShortnameReferences } from "@converse/headless/converse-emoji.js";
+import { MessageText } from '../../shared/message/text.js';
+import { api, converse } from "@converse/headless/converse-core";
import { directive, html } from "lit-html";
import { until } from 'lit-html/directives/until.js';
-const u = converse.env.utils;
-
-
-/**
- * @class MessageText
- * A String subclass that is used to represent the rich text
- * of a chat message.
- *
- * The "rich" parts of the text is represented by lit-html TemplateResult
- * objects which are added via the {@link MessageText.addTemplateResult}
- * method and saved as metadata.
- *
- * By default Converse adds TemplateResults to support emojis, hyperlinks,
- * images, map URIs and mentions.
- *
- * 3rd party plugins can listen for the `beforeMessageBodyTransformed`
- * and/or `afterMessageBodyTransformed` events and then call
- * `addTemplateResult` on the MessageText instance in order to add their own
- * rich features.
- */
-class MessageText extends String {
-
- /**
- * Create a new {@link MessageText} instance.
- * @param { String } text - The plain text that was received from the `` stanza.
- */
- constructor (text) {
- super(text);
- this.references = [];
- }
-
- /**
- * The "rich" markup parts of a chat message are represented by lit-html
- * TemplateResult objects.
- *
- * This method can be used to add new template results to this message's
- * text.
- *
- * @method MessageText.addTemplateResult
- * @param { Number } begin - The starting index of the plain message text
- * which is being replaced with markup.
- * @param { Number } end - The ending index of the plain message text
- * which is being replaced with markup.
- * @param { Object } template - The lit-html TemplateResult instance
- */
- addTemplateResult (begin, end, template) {
- this.references.push({begin, end, template});
- }
-
- isMeCommand () {
- const text = this.toString();
- if (!text) {
- return false;
- }
- return text.startsWith('/me ');
- }
-
- static replaceText (text) {
- return convertASCII2Emoji(text.replace(/\n\n+/g, '\n\n'));
- }
-
- marshall () {
- let list = [this.toString()];
- this.references
- .sort((a, b) => b.begin - a.begin)
- .forEach(ref => {
- const text = list.shift();
- list = [
- text.slice(0, ref.begin),
- ref.template,
- text.slice(ref.end),
- ...list
- ];
- });
-
- // Subtract `/me ` from 3rd person messages
- if (this.isMeCommand()) list[0] = list[0].substring(4);
-
- const isString = (s) => typeof s === 'string';
- return list.reduce((acc, i) => isString(i) ? [...acc, MessageText.replaceText(i)] : [...acc, i], []);
- }
-}
-
-
-function addMapURLs (text) {
- const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
- const matches = text.matchAll(regex);
- for (const m of matches) {
- text.addTemplateResult(
- m.index,
- m.index+m.input.length,
- u.convertUrlToHyperlink(m.input.replace(regex, _converse.geouri_replacement))
- );
- }
-}
-
-
-function addHyperlinks (text, onImgLoad, onImgClick) {
- const objs = [];
- try {
- const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
- URI.withinString(text, (url, start, end) => {
- objs.push({url, start, end})
- return url;
- } , parse_options);
- } catch (error) {
- log.debug(error);
- return;
- }
- const show_images = api.settings.get('show_images_inline');
- objs.forEach(url_obj => {
- const url_text = text.slice(url_obj.start, url_obj.end);
- const filtered_url = u.filterQueryParamsFromURL(url_text);
- text.addTemplateResult(
- url_obj.start,
- url_obj.end,
- show_images && u.isImageURL(url_text) && u.isImageDomainAllowed(url_text) ?
- u.convertToImageTag(filtered_url, onImgLoad, onImgClick) :
- u.convertUrlToHyperlink(filtered_url),
- );
- });
-}
-
-
-async function addEmojis (text) {
- await api.emojis.initialize();
- const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())];
- references.forEach(e => {
- text.addTemplateResult(
- e.begin,
- e.end,
- getEmojiMarkup(e, {'add_title_wrapper': true})
- );
- });
-}
-
-
-const tpl_mention_with_nick = (o) => html`${o.mention}`;
-const tpl_mention = (o) => html`${o.mention}`;
-
-function addReferences (text, model) {
- if (!model.collection) {
- // This model doesn't belong to a collection anymore, so it must be
- // have been removed in the meantime and can be ignored.
- log.debug('addReferences: ignoring dangling model');
- return;
- }
- const nick = model.collection.chatbox.get('nick');
- model.get('references')?.forEach(ref => {
- const mention = text.slice(ref.begin, ref.end);
- if (mention === nick) {
- text.addTemplateResult(ref.begin, ref.end, tpl_mention_with_nick({mention}));
- } else {
- text.addTemplateResult(ref.begin, ref.end, tpl_mention({mention}));
- }
- });
-}
+const u = converse.env.utils;
class MessageBodyRenderer {
@@ -186,42 +28,18 @@ class MessageBodyRenderer {
}
async transform () {
- const text = new MessageText(this.text);
- /**
- * Synchronous event which provides a hook for transforming a chat message's body text
- * before the default transformations have been applied.
- * @event _converse#beforeMessageBodyTransformed
- * @param { _converse.Message } model - The model representing the message
- * @param { MessageText } text - A {@link MessageText } instance. You
- * can call {@link MessageText#addTemplateResult } on it in order to
- * add TemplateResult objects meant to render rich parts of the
- * message.
- * @example _converse.api.listen.on('beforeMessageBodyTransformed', (view, text) => { ... });
- */
- await api.trigger('beforeMessageBodyTransformed', this.model, text, {'Synchronous': true});
-
- addHyperlinks(
- text,
+ const show_images = api.settings.get('show_images_inline');
+ const offset = 0;
+ const text = new MessageText(
+ this.text,
+ this.model,
+ offset,
+ show_images,
() => this.scrollDownOnImageLoad(),
ev => this.component.showImageModal(ev)
);
- addMapURLs(text);
- await addEmojis(text);
- addReferences(text, this.model);
-
- /**
- * Synchronous event which provides a hook for transforming a chat message's body text
- * after the default transformations have been applied.
- * @event _converse#afterMessageBodyTransformed
- * @param { _converse.Message } model - The model representing the message
- * @param { MessageText } text - A {@link MessageText } instance. You
- * can call {@link MessageText#addTemplateResult} on it in order to
- * add TemplateResult objects meant to render rich parts of the
- * message.
- * @example _converse.api.listen.on('afterMessageBodyTransformed', (view, text) => { ... });
- */
- await api.trigger('afterMessageBodyTransformed', this.model, text, {'Synchronous': true});
- return text.marshall();
+ await text.addTemplates();
+ return text.payload;
}
render () {
diff --git a/src/templates/directives/styling.js b/src/templates/directives/styling.js
new file mode 100644
index 0000000000..ec5c73f2b5
--- /dev/null
+++ b/src/templates/directives/styling.js
@@ -0,0 +1,16 @@
+import { MessageText } from '../../shared/message/text.js';
+import { directive, html } from "lit-html";
+import { until } from 'lit-html/directives/until.js';
+
+
+async function transform (t) {
+ await t.addTemplates();
+ return t.payload;
+}
+
+function renderer (text, model, offset) {
+ const t = new MessageText(text, model, offset, false);
+ return html`${until(transform(t), html`${t}`)}`;
+}
+
+export const renderStylingDirectiveBody = directive((text, model, offset) => p => p.setValue(renderer(text, model, offset)));