瀏覽代碼

Allow selected characters to precede a mention

Xavi Ferrer 4 年之前
父節點
當前提交
8b9c97745f
共有 6 個文件被更改,包括 80 次插入11 次删除
  1. 1 0
      CHANGES.md
  2. 54 0
      spec/autocomplete.js
  3. 12 4
      src/converse-autocomplete.js
  4. 3 1
      src/headless/converse-muc.js
  5. 9 5
      src/headless/utils/core.js
  6. 1 1
      src/headless/utils/parse-helpers.js

+ 1 - 0
CHANGES.md

@@ -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)
 

+ 54 - 0
spec/autocomplete.js

@@ -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');

+ 12 - 4
src/converse-autocomplete.js

@@ -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');
             }
         }
 

+ 3 - 1
src/headless/converse-muc.js

@@ -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) {

+ 9 - 5
src/headless/utils/core.js

@@ -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) {

+ 1 - 1
src/headless/utils/parse-helpers.js

@@ -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);