autocomplete: Use regex instead of hardcoded list...
to determine valid characters to form a boundary before an `@` mention Also fixed an issue with mentions looking like they're part of URLs, by first processing mentions separately.
This commit is contained in:
parent
8b9c97745f
commit
f86efca9a6
@ -3,7 +3,7 @@
|
|||||||
## 8.0.0 (Unreleased)
|
## 8.0.0 (Unreleased)
|
||||||
|
|
||||||
- #1083: Add support for XEP-0393 Message Styling
|
- #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
|
- 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)
|
- New configuration setting: [show_tab_notifications](https://conversejs.org/docs/html/configuration.html#show-tab-notifications)
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ describe("An incoming groupchat message", function () {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const msg = $msg({
|
let msg = $msg({
|
||||||
from: 'lounge@montague.lit/gibson',
|
from: 'lounge@montague.lit/gibson',
|
||||||
id: u.getUniqueId(),
|
id: u.getUniqueId(),
|
||||||
to: 'romeo@montague.lit',
|
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':'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;
|
.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);
|
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 <span class="mention">z3r0</span> '+
|
||||||
|
'<span class="mention mention--self badge badge-info">tom</span> '+
|
||||||
|
'<span class="mention">mr.robot</span>, 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.classList.length).toEqual(1);
|
||||||
expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
|
expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
|
||||||
'hello <span class="mention">z3r0</span> '+
|
'hello <span class="mention">z3r0</span> '+
|
||||||
@ -139,8 +154,7 @@ describe("A sent groupchat message", function () {
|
|||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also check that nicks from received messages, (but for which
|
// Also check that nicks from received messages, (but for which we don't have occupant objects) can be mentioned.
|
||||||
// we don't have occupant objects) can be mentioned.
|
|
||||||
const stanza = u.toStanza(`
|
const stanza = u.toStanza(`
|
||||||
<message xmlns="jabber:client"
|
<message xmlns="jabber:client"
|
||||||
from="${muc_jid}/gh0st"
|
from="${muc_jid}/gh0st"
|
||||||
@ -200,8 +214,7 @@ describe("A sent groupchat message", function () {
|
|||||||
[text, references] = view.model.parseTextForReferences('https://example.org/@gibson')
|
[text, references] = view.model.parseTextForReferences('https://example.org/@gibson')
|
||||||
expect(text).toBe('https://example.org/@gibson');
|
expect(text).toBe('https://example.org/@gibson');
|
||||||
expect(references.length).toBe(0);
|
expect(references.length).toBe(0);
|
||||||
expect(references)
|
expect(references).toEqual([]);
|
||||||
.toEqual([]);
|
|
||||||
|
|
||||||
[text, references] = view.model.parseTextForReferences('mail@gibson.com')
|
[text, references] = view.model.parseTextForReferences('mail@gibson.com')
|
||||||
expect(text).toBe('mail@gibson.com');
|
expect(text).toBe('mail@gibson.com');
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
import { Events } from '@converse/skeletor/src/events.js';
|
import { Events } from '@converse/skeletor/src/events.js';
|
||||||
import { converse } from "@converse/headless/converse-core";
|
import { converse } from "@converse/headless/converse-core";
|
||||||
|
|
||||||
converse.MENTION_BOUNDARIES = ['"', '(', '<', '#', '!', '\\', '/', '+', '~', '[', '{', '^', '>'];
|
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
|
||||||
@ -96,9 +95,8 @@ const helpers = {
|
|||||||
return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
|
return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
|
||||||
},
|
},
|
||||||
|
|
||||||
isMention (word, ac_triggers, mention_boundaries) {
|
isMention (word, ac_triggers) {
|
||||||
return (ac_triggers.includes(word[0]) ||
|
return (ac_triggers.includes(word[0]) || u.isMentionBoundary(word[0]) && ac_triggers.includes(word[1]));
|
||||||
(mention_boundaries.includes(word[0]) && ac_triggers.includes(word[1])));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +249,7 @@ export class AutoComplete {
|
|||||||
|
|
||||||
insertValue (suggestion) {
|
insertValue (suggestion) {
|
||||||
if (this.match_current_word) {
|
if (this.match_current_word) {
|
||||||
u.replaceCurrentWord(this.input, suggestion.value, converse.MENTION_BOUNDARIES);
|
u.replaceCurrentWord(this.input, suggestion.value);
|
||||||
} else {
|
} else {
|
||||||
this.input.value = suggestion.value;
|
this.input.value = suggestion.value;
|
||||||
}
|
}
|
||||||
@ -371,7 +369,7 @@ export class AutoComplete {
|
|||||||
this.auto_completing = true;
|
this.auto_completing = true;
|
||||||
} else if (ev.key === "Backspace") {
|
} else if (ev.key === "Backspace") {
|
||||||
const word = u.getCurrentWord(ev.target, ev.target.selectionEnd-1);
|
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;
|
this.auto_completing = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -393,11 +391,11 @@ export class AutoComplete {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
|
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) {
|
if (contains_trigger) {
|
||||||
this.auto_completing = true;
|
this.auto_completing = true;
|
||||||
if (!this.include_triggers.includes(ev.key)) {
|
if (!this.include_triggers.includes(ev.key)) {
|
||||||
value = converse.MENTION_BOUNDARIES.includes(value[0])
|
value = u.isMentionBoundary(value[0])
|
||||||
? value.slice('2')
|
? value.slice('2')
|
||||||
: value.slice('1');
|
: value.slice('1');
|
||||||
}
|
}
|
||||||
@ -445,5 +443,3 @@ converse.plugins.add("converse-autocomplete", {
|
|||||||
_converse.AutoComplete = AutoComplete;
|
_converse.AutoComplete = AutoComplete;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import p from "./utils/parse-helpers";
|
|||||||
export const ROLES = ['moderator', 'participant', 'visitor'];
|
export const ROLES = ['moderator', 'participant', 'visitor'];
|
||||||
export const AFFILIATIONS = ['owner', 'admin', 'member', 'outcast', 'none'];
|
export const AFFILIATIONS = ['owner', 'admin', 'member', 'outcast', 'none'];
|
||||||
|
|
||||||
|
|
||||||
converse.MUC_TRAFFIC_STATES = ['entered', 'exited'];
|
converse.MUC_TRAFFIC_STATES = ['entered', 'exited'];
|
||||||
converse.MUC_ROLE_CHANGES = ['op', 'deop', 'voice', 'mute'];
|
converse.MUC_ROLE_CHANGES = ['op', 'deop', 'voice', 'mute'];
|
||||||
|
|
||||||
@ -963,9 +964,7 @@ converse.plugins.add('converse-muc', {
|
|||||||
getAllKnownNicknamesRegex () {
|
getAllKnownNicknamesRegex () {
|
||||||
const longNickString = this.getAllKnownNicknames().join('|');
|
const longNickString = this.getAllKnownNicknames().join('|');
|
||||||
const escapedLongNickString = p.escapeRegexString(longNickString)
|
const escapedLongNickString = p.escapeRegexString(longNickString)
|
||||||
const mention_boundaries = converse.MENTION_BOUNDARIES.join('|');
|
return RegExp(`(?:\\p{P}|\\p{Z}|^)@(${escapedLongNickString})(?![\\w@-])`, 'uig');
|
||||||
const escaped_mention_boundaries = p.escapeRegexString(mention_boundaries);
|
|
||||||
return RegExp(`(?:\\s|^)[${escaped_mention_boundaries}]?@(${escapedLongNickString})(?![\\w@-])`, 'ig');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getOccupantByJID (jid) {
|
getOccupantByJID (jid) {
|
||||||
@ -976,14 +975,18 @@ converse.plugins.add('converse-muc', {
|
|||||||
return this.occupants.findOccupant({ nick });
|
return this.occupants.findOccupant({ nick });
|
||||||
},
|
},
|
||||||
|
|
||||||
parseTextForReferences (original_message) {
|
/**
|
||||||
if (!original_message) return ['', []];
|
* Given a text message, look for `@` mentions and turn them into
|
||||||
const findRegexInMessage = p.matchRegexInText(original_message);
|
* XEP-0372 references
|
||||||
const raw_mentions = findRegexInMessage(p.mention_regex);
|
* @param { String } text
|
||||||
if (!raw_mentions) return [original_message, []];
|
*/
|
||||||
|
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(this.getAllKnownNicknames());
|
||||||
const getMatchingNickname = p.findFirstMatchInArray(known_nicknames);
|
|
||||||
|
|
||||||
const uriFromNickname = nickname => {
|
const uriFromNickname = nickname => {
|
||||||
const jid = this.get('jid');
|
const jid = this.get('jid');
|
||||||
@ -1002,11 +1005,12 @@ converse.plugins.add('converse-muc', {
|
|||||||
return { begin, end, value, type, uri }
|
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 references = mentions.map(matchToReference);
|
||||||
|
|
||||||
const [updated_message, updated_references] = p.reduceTextFromReferences(
|
const [updated_message, updated_references] = p.reduceTextFromReferences(
|
||||||
original_message,
|
text,
|
||||||
references
|
references
|
||||||
);
|
);
|
||||||
return [updated_message, updated_references];
|
return [updated_message, updated_references];
|
||||||
|
@ -425,13 +425,13 @@ u.getCurrentWord = function (input, index, delineator) {
|
|||||||
return word;
|
return word;
|
||||||
};
|
};
|
||||||
|
|
||||||
u.replaceCurrentWord = function (input, new_value, mention_boundaries=[]) {
|
u.isMentionBoundary = (s) => s !== '@' && RegExp(`(\\p{Z}|\\p{P})`, 'u').test(s);
|
||||||
const caret = input.selectionEnd || undefined,
|
|
||||||
current_word = last(input.value.slice(0, caret).split(/\s/)),
|
u.replaceCurrentWord = function (input, new_value) {
|
||||||
value = input.value,
|
const caret = input.selectionEnd || undefined;
|
||||||
mention_boundary = mention_boundaries.includes(current_word[0])
|
const current_word = last(input.value.slice(0, caret).split(/\s/));
|
||||||
? current_word[0]
|
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);
|
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;
|
const selection_end = caret - current_word.length + new_value.length + 1;
|
||||||
input.selectionEnd = mention_boundary ? selection_end + 1 : selection_end;
|
input.selectionEnd = mention_boundary ? selection_end + 1 : selection_end;
|
||||||
|
@ -6,11 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
const helpers = {};
|
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);
|
const escapeRegexChars = (string, char) => string.replace(RegExp('\\' + char, 'ig'), '\\' + char);
|
||||||
|
|
||||||
helpers.escapeCharacters = characters => string =>
|
helpers.escapeCharacters = characters => string =>
|
||||||
|
@ -112,8 +112,7 @@ export class MessageText extends String {
|
|||||||
* @param { Integer } offset - The index of the passed in text relative to
|
* @param { Integer } offset - The index of the passed in text relative to
|
||||||
* the start of the message body text.
|
* the start of the message body text.
|
||||||
*/
|
*/
|
||||||
async addEmojis (text, offset) {
|
addEmojis (text, offset) {
|
||||||
await api.emojis.initialize();
|
|
||||||
const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())];
|
const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())];
|
||||||
references.forEach(e => {
|
references.forEach(e => {
|
||||||
this.addTemplateResult(
|
this.addTemplateResult(
|
||||||
@ -129,9 +128,10 @@ export class MessageText extends String {
|
|||||||
* rendering them.
|
* rendering them.
|
||||||
* @param { String } text
|
* @param { String } text
|
||||||
* @param { Integer } offset - The index of the passed in text relative to
|
* @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) {
|
addMentions (text, offset) {
|
||||||
|
offset += this.offset;
|
||||||
if (!this.model.collection) {
|
if (!this.model.collection) {
|
||||||
// This model doesn't belong to a collection anymore, so it must be
|
// This model doesn't belong to a collection anymore, so it must be
|
||||||
// have been removed in the meantime and can be ignored.
|
// 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.
|
* 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});
|
await api.trigger('beforeMessageBodyTransformed', this, {'Synchronous': true});
|
||||||
|
|
||||||
this.addStyling();
|
this.addStyling();
|
||||||
const payload = this.marshall();
|
this.addReferences(this.addMentions);
|
||||||
let idx = 0; // The text index of the element in the payload
|
this.addReferences(this.addHyperlinks);
|
||||||
for (const text of payload) {
|
this.addReferences(this.addMapURLs);
|
||||||
if (!text) {
|
|
||||||
continue
|
await api.emojis.initialize();
|
||||||
} else if (isString(text)) {
|
this.addReferences(this.addEmojis);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronous event which provides a hook for transforming a chat message's body text
|
* Synchronous event which provides a hook for transforming a chat message's body text
|
||||||
|
Loading…
Reference in New Issue
Block a user