Commit 8b9c9774 authored by Xavi Ferrer's avatar Xavi Ferrer Committed by JC Brand

Allow selected characters to precede a mention

parent 35db01d3
...@@ -3,6 +3,7 @@ ...@@ -3,6 +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
- 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)
......
...@@ -114,6 +114,60 @@ describe("The nickname autocomplete feature", function () { ...@@ -114,6 +114,60 @@ describe("The nickname autocomplete feature", function () {
done(); done();
})); }));
it("shows all autocompletion options when the user presses @ right after an allowed character",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {'opening_mention_characters':['(']},
async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
const view = _converse.chatboxviews.get('lounge@montague.lit');
// Nicknames from presences
['dick', 'harry'].forEach((nick) => {
_converse.connection._dataRecv(mock.createRequest(
$pres({
'to': 'tom@montague.lit/resource',
'from': `lounge@montague.lit/${nick}`
})
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': `${nick}@montague.lit/resource`,
'role': 'participant'
})));
});
// Nicknames from messages
const msg = $msg({
from: 'lounge@montague.lit/jane',
id: u.getUniqueId(),
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t('Hello world').tree();
await view.model.handleMessageStanza(msg);
// Test that pressing @ brings up all options
const textarea = view.el.querySelector('textarea.chat-textarea');
const at_event = {
'target': textarea,
'preventDefault': function preventDefault () {},
'stopPropagation': function stopPropagation () {},
'keyCode': 50,
'key': '@'
};
textarea.value = '('
view.onKeyDown(at_event);
textarea.value = '(@';
view.onKeyUp(at_event);
await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
expect(view.el.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
done();
}));
it("should order by query index position and length", mock.initConverse( it("should order by query index position and length", mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
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;
...@@ -93,6 +94,11 @@ const helpers = { ...@@ -93,6 +94,11 @@ const helpers = {
regExpEscape (s) { regExpEscape (s) {
return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); 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])));
} }
} }
...@@ -245,7 +251,7 @@ export class AutoComplete { ...@@ -245,7 +251,7 @@ export class AutoComplete {
insertValue (suggestion) { insertValue (suggestion) {
if (this.match_current_word) { if (this.match_current_word) {
u.replaceCurrentWord(this.input, suggestion.value); u.replaceCurrentWord(this.input, suggestion.value, converse.MENTION_BOUNDARIES);
} else { } else {
this.input.value = suggestion.value; this.input.value = suggestion.value;
} }
...@@ -365,7 +371,7 @@ export class AutoComplete { ...@@ -365,7 +371,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 (this.ac_triggers.includes(word[0])) { if (helpers.isMention(word, this.ac_triggers, converse.MENTION_BOUNDARIES)) {
this.auto_completing = true; this.auto_completing = true;
} }
} }
...@@ -387,11 +393,13 @@ export class AutoComplete { ...@@ -387,11 +393,13 @@ 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 = this.ac_triggers.includes(value[0]); const contains_trigger = helpers.isMention(value, this.ac_triggers, converse.MENTION_BOUNDARIES);
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 = value.slice('1'); value = converse.MENTION_BOUNDARIES.includes(value[0])
? value.slice('2')
: value.slice('1');
} }
} }
......
...@@ -963,7 +963,9 @@ converse.plugins.add('converse-muc', { ...@@ -963,7 +963,9 @@ 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)
return RegExp(`(?:\\s|^)@(${escapedLongNickString})(?![\\w@-])`, 'ig'); const mention_boundaries = converse.MENTION_BOUNDARIES.join('|');
const escaped_mention_boundaries = p.escapeRegexString(mention_boundaries);
return RegExp(`(?:\\s|^)[${escaped_mention_boundaries}]?@(${escapedLongNickString})(?![\\w@-])`, 'ig');
}, },
getOccupantByJID (jid) { getOccupantByJID (jid) {
......
...@@ -425,12 +425,16 @@ u.getCurrentWord = function (input, index, delineator) { ...@@ -425,12 +425,16 @@ u.getCurrentWord = function (input, index, delineator) {
return word; return word;
}; };
u.replaceCurrentWord = function (input, new_value) { u.replaceCurrentWord = function (input, new_value, mention_boundaries=[]) {
const caret = input.selectionEnd || undefined, const caret = input.selectionEnd || undefined,
current_word = last(input.value.slice(0, caret).split(' ')), current_word = last(input.value.slice(0, caret).split(/\s/)),
value = input.value; value = input.value,
input.value = value.slice(0, caret - current_word.length) + `${new_value} ` + value.slice(caret); mention_boundary = mention_boundaries.includes(current_word[0])
input.selectionEnd = caret - current_word.length + new_value.length + 1; ? 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;
}; };
u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) { u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
const helpers = {}; const helpers = {};
// Captures all mentions, but includes a space before the @ // Captures all mentions, but includes a space before the @
helpers.mention_regex = /(?:\s|^)([@][\w_-]+(?:\.\w+)*)/ig; helpers.mention_regex = /(?:\s|^)([@][\w_-]+(?:\.\w+)*)/gi;
helpers.matchRegexInText = text => regex => text.matchAll(regex); helpers.matchRegexInText = text => regex => text.matchAll(regex);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment