diff --git a/CHANGES.md b/CHANGES.md
index 3671710fd..be21f0b7b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,7 +3,7 @@
## 8.0.0 (Unreleased)
- #1083: Add support for XEP-0393 Message Styling
-- #2275: Allow selected characters to precede a mention
+- #2275: Allow punctuation to immediately precede a mention
- Bugfix: `null` inserted by emoji picker and can't switch between skintones
- New configuration setting: [show_tab_notifications](https://conversejs.org/docs/html/configuration.html#show-tab-notifications)
diff --git a/spec/mentions.js b/spec/mentions.js
index a88a8e9a1..544e87aa1 100644
--- a/spec/mentions.js
+++ b/spec/mentions.js
@@ -52,7 +52,7 @@ describe("An incoming groupchat message", function () {
}))
);
});
- const msg = $msg({
+ let msg = $msg({
from: 'lounge@montague.lit/gibson',
id: u.getUniqueId(),
to: 'romeo@montague.lit',
@@ -62,7 +62,22 @@ describe("An incoming groupchat message", function () {
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree;
await view.model.handleMessageStanza(msg);
- const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
+ let 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?');
+
+ msg = $msg({
+ from: 'lounge@montague.lit/sw0rdf1sh',
+ id: u.getUniqueId(),
+ to: 'romeo@montague.lit',
+ type: 'groupchat'
+ }).c('body').t('https://conversejs.org/@gibson').up()
+ .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'23', 'end':'29', 'type':'mention', 'uri':'xmpp:gibson@montague.lit'}).nodeTree;
+ await view.model.handleMessageStanza(msg);
+ message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
expect(message.classList.length).toEqual(1);
expect(message.innerHTML.replace(//g, '')).toBe(
'hello z3r0 '+
@@ -139,8 +154,7 @@ describe("A sent groupchat message", function () {
})));
});
- // Also check that nicks from received messages, (but for which
- // we don't have occupant objects) can be mentioned.
+ // Also check that nicks from received messages, (but for which we don't have occupant objects) can be mentioned.
const stanza = u.toStanza(`
'];
const u = converse.env.utils;
@@ -96,9 +95,8 @@ const helpers = {
return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
},
- isMention (word, ac_triggers, mention_boundaries) {
- return (ac_triggers.includes(word[0]) ||
- (mention_boundaries.includes(word[0]) && ac_triggers.includes(word[1])));
+ isMention (word, ac_triggers) {
+ return (ac_triggers.includes(word[0]) || u.isMentionBoundary(word[0]) && ac_triggers.includes(word[1]));
}
}
@@ -251,7 +249,7 @@ export class AutoComplete {
insertValue (suggestion) {
if (this.match_current_word) {
- u.replaceCurrentWord(this.input, suggestion.value, converse.MENTION_BOUNDARIES);
+ u.replaceCurrentWord(this.input, suggestion.value);
} else {
this.input.value = suggestion.value;
}
@@ -371,7 +369,7 @@ export class AutoComplete {
this.auto_completing = true;
} else if (ev.key === "Backspace") {
const word = u.getCurrentWord(ev.target, ev.target.selectionEnd-1);
- if (helpers.isMention(word, this.ac_triggers, converse.MENTION_BOUNDARIES)) {
+ if (helpers.isMention(word, this.ac_triggers)) {
this.auto_completing = true;
}
}
@@ -393,11 +391,11 @@ export class AutoComplete {
}
let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
- const contains_trigger = helpers.isMention(value, this.ac_triggers, converse.MENTION_BOUNDARIES);
+ const contains_trigger = helpers.isMention(value, this.ac_triggers);
if (contains_trigger) {
this.auto_completing = true;
if (!this.include_triggers.includes(ev.key)) {
- value = converse.MENTION_BOUNDARIES.includes(value[0])
+ value = u.isMentionBoundary(value[0])
? value.slice('2')
: value.slice('1');
}
@@ -445,5 +443,3 @@ converse.plugins.add("converse-autocomplete", {
_converse.AutoComplete = AutoComplete;
}
});
-
-
diff --git a/src/headless/converse-muc.js b/src/headless/converse-muc.js
index 7e3b78800..d98c6b7a8 100644
--- a/src/headless/converse-muc.js
+++ b/src/headless/converse-muc.js
@@ -20,6 +20,7 @@ import p from "./utils/parse-helpers";
export const ROLES = ['moderator', 'participant', 'visitor'];
export const AFFILIATIONS = ['owner', 'admin', 'member', 'outcast', 'none'];
+
converse.MUC_TRAFFIC_STATES = ['entered', 'exited'];
converse.MUC_ROLE_CHANGES = ['op', 'deop', 'voice', 'mute'];
@@ -963,9 +964,7 @@ converse.plugins.add('converse-muc', {
getAllKnownNicknamesRegex () {
const longNickString = this.getAllKnownNicknames().join('|');
const escapedLongNickString = p.escapeRegexString(longNickString)
- const mention_boundaries = converse.MENTION_BOUNDARIES.join('|');
- const escaped_mention_boundaries = p.escapeRegexString(mention_boundaries);
- return RegExp(`(?:\\s|^)[${escaped_mention_boundaries}]?@(${escapedLongNickString})(?![\\w@-])`, 'ig');
+ return RegExp(`(?:\\p{P}|\\p{Z}|^)@(${escapedLongNickString})(?![\\w@-])`, 'uig');
},
getOccupantByJID (jid) {
@@ -976,14 +975,18 @@ converse.plugins.add('converse-muc', {
return this.occupants.findOccupant({ nick });
},
- parseTextForReferences (original_message) {
- if (!original_message) return ['', []];
- const findRegexInMessage = p.matchRegexInText(original_message);
- const raw_mentions = findRegexInMessage(p.mention_regex);
- if (!raw_mentions) return [original_message, []];
+ /**
+ * Given a text message, look for `@` mentions and turn them into
+ * XEP-0372 references
+ * @param { String } text
+ */
+ parseTextForReferences (text) {
+ const mentions_regex = /(\p{P}|\p{Z}|^)([@][\w_-]+(?:\.\w+)*)/ugi;
+ if (!text || !mentions_regex.test(text)) {
+ return [text, []];
+ }
- const known_nicknames = this.getAllKnownNicknames();
- const getMatchingNickname = p.findFirstMatchInArray(known_nicknames);
+ const getMatchingNickname = p.findFirstMatchInArray(this.getAllKnownNicknames());
const uriFromNickname = nickname => {
const jid = this.get('jid');
@@ -1002,11 +1005,12 @@ converse.plugins.add('converse-muc', {
return { begin, end, value, type, uri }
}
- const mentions = [...findRegexInMessage(this.getAllKnownNicknamesRegex())];
+ const regex = this.getAllKnownNicknamesRegex();
+ const mentions = [...text.matchAll(regex)].filter(m => !m[0].startsWith('/'));
const references = mentions.map(matchToReference);
const [updated_message, updated_references] = p.reduceTextFromReferences(
- original_message,
+ text,
references
);
return [updated_message, updated_references];
diff --git a/src/headless/utils/core.js b/src/headless/utils/core.js
index d3b6c9d19..bd606d195 100644
--- a/src/headless/utils/core.js
+++ b/src/headless/utils/core.js
@@ -425,13 +425,13 @@ u.getCurrentWord = function (input, index, delineator) {
return word;
};
-u.replaceCurrentWord = function (input, new_value, mention_boundaries=[]) {
- const caret = input.selectionEnd || undefined,
- current_word = last(input.value.slice(0, caret).split(/\s/)),
- value = input.value,
- mention_boundary = mention_boundaries.includes(current_word[0])
- ? current_word[0]
- : '';
+u.isMentionBoundary = (s) => s !== '@' && RegExp(`(\\p{Z}|\\p{P})`, 'u').test(s);
+
+u.replaceCurrentWord = function (input, new_value) {
+ const caret = input.selectionEnd || undefined;
+ const current_word = last(input.value.slice(0, caret).split(/\s/));
+ const value = input.value;
+ const mention_boundary = u.isMentionBoundary(current_word[0]) ? current_word[0] : '';
input.value = value.slice(0, caret - current_word.length) + mention_boundary + `${new_value} ` + value.slice(caret);
const selection_end = caret - current_word.length + new_value.length + 1;
input.selectionEnd = mention_boundary ? selection_end + 1 : selection_end;
diff --git a/src/headless/utils/parse-helpers.js b/src/headless/utils/parse-helpers.js
index 341c3a08e..457b4d813 100644
--- a/src/headless/utils/parse-helpers.js
+++ b/src/headless/utils/parse-helpers.js
@@ -6,11 +6,6 @@
*/
const helpers = {};
-// Captures all mentions, but includes a space before the @
-helpers.mention_regex = /(?:\s|^)([@][\w_-]+(?:\.\w+)*)/gi;
-
-helpers.matchRegexInText = text => regex => text.matchAll(regex);
-
const escapeRegexChars = (string, char) => string.replace(RegExp('\\' + char, 'ig'), '\\' + char);
helpers.escapeCharacters = characters => string =>
diff --git a/src/shared/message/text.js b/src/shared/message/text.js
index c49af3fc1..4c1b3de8b 100644
--- a/src/shared/message/text.js
+++ b/src/shared/message/text.js
@@ -112,8 +112,7 @@ export class MessageText extends String {
* @param { Integer } offset - The index of the passed in text relative to
* the start of the message body text.
*/
- async addEmojis (text, offset) {
- await api.emojis.initialize();
+ addEmojis (text, offset) {
const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())];
references.forEach(e => {
this.addTemplateResult(
@@ -129,9 +128,10 @@ export class MessageText extends String {
* rendering them.
* @param { String } text
* @param { Integer } offset - The index of the passed in text relative to
- * the start of the message body text.
+ * the start of the MessageText.
*/
addMentions (text, offset) {
+ offset += this.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.
@@ -193,6 +193,28 @@ export class MessageText extends String {
}
}
+
+ /**
+ * Look for plaintext (i.e. non-templated) sections of this MessageText
+ * instance and add references via the passed in function.
+ * @param { Function } func
+ */
+ addReferences (func) {
+ const payload = this.marshall();
+ let idx = 0; // The text index of the element in the payload
+ for (const text of payload) {
+ if (!text) {
+ continue
+ } else if (isString(text)) {
+ func.call(this, text, idx);
+ idx += text.length;
+ } else {
+ idx = text.end;
+ }
+ }
+ }
+
+
/**
* Parse the text and add template references for rendering the "rich" parts.
*
@@ -214,21 +236,12 @@ export class MessageText extends String {
await api.trigger('beforeMessageBodyTransformed', this, {'Synchronous': true});
this.addStyling();
- const payload = this.marshall();
- let idx = 0; // The text index of the element in the payload
- for (const text of payload) {
- if (!text) {
- continue
- } else if (isString(text)) {
- this.addHyperlinks(text, idx);
- this.addMapURLs(text, idx);
- await this.addEmojis(text, idx);
- this.addMentions(text, this.offset+idx);
- idx += text.length;
- } else {
- idx += text.end;
- }
- }
+ this.addReferences(this.addMentions);
+ this.addReferences(this.addHyperlinks);
+ this.addReferences(this.addMapURLs);
+
+ await api.emojis.initialize();
+ this.addReferences(this.addEmojis);
/**
* Synchronous event which provides a hook for transforming a chat message's body text