Allow selected characters to precede a mention

This commit is contained in:
Xavi Ferrer 2020-10-07 18:51:11 +02:00 committed by JC Brand
parent 35db01d316
commit 8b9c97745f
6 changed files with 80 additions and 11 deletions

View File

@ -3,6 +3,7 @@
## 8.0.0 (Unreleased)
- #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
- New configuration setting: [show_tab_notifications](https://conversejs.org/docs/html/configuration.html#show-tab-notifications)

View File

@ -114,6 +114,60 @@ describe("The nickname autocomplete feature", function () {
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(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');

View File

@ -9,6 +9,7 @@
import { Events } from '@converse/skeletor/src/events.js';
import { converse } from "@converse/headless/converse-core";
converse.MENTION_BOUNDARIES = ['"', '(', '<', '#', '!', '\\', '/', '+', '~', '[', '{', '^', '>'];
const u = converse.env.utils;
@ -93,6 +94,11 @@ const helpers = {
regExpEscape (s) {
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 {
insertValue (suggestion) {
if (this.match_current_word) {
u.replaceCurrentWord(this.input, suggestion.value);
u.replaceCurrentWord(this.input, suggestion.value, converse.MENTION_BOUNDARIES);
} else {
this.input.value = suggestion.value;
}
@ -365,7 +371,7 @@ export class AutoComplete {
this.auto_completing = true;
} else if (ev.key === "Backspace") {
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;
}
}
@ -387,11 +393,13 @@ export class AutoComplete {
}
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) {
this.auto_completing = true;
if (!this.include_triggers.includes(ev.key)) {
value = value.slice('1');
value = converse.MENTION_BOUNDARIES.includes(value[0])
? value.slice('2')
: value.slice('1');
}
}

View File

@ -963,7 +963,9 @@ converse.plugins.add('converse-muc', {
getAllKnownNicknamesRegex () {
const longNickString = this.getAllKnownNicknames().join('|');
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) {

View File

@ -425,12 +425,16 @@ u.getCurrentWord = function (input, index, delineator) {
return word;
};
u.replaceCurrentWord = function (input, new_value) {
u.replaceCurrentWord = function (input, new_value, mention_boundaries=[]) {
const caret = input.selectionEnd || undefined,
current_word = last(input.value.slice(0, caret).split(' ')),
value = input.value;
input.value = value.slice(0, caret - current_word.length) + `${new_value} ` + value.slice(caret);
input.selectionEnd = caret - current_word.length + new_value.length + 1;
current_word = last(input.value.slice(0, caret).split(/\s/)),
value = input.value,
mention_boundary = mention_boundaries.includes(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;
};
u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {

View File

@ -7,7 +7,7 @@
const helpers = {};
// 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);