2
0
Эх сурвалжийг харах

Makes mentions case-insensitive, plus parsing functionality refactor (#2061)

Ariel Fuggini 5 жил өмнө
parent
commit
73d33e1161

+ 5 - 5
package-lock.json

@@ -13293,9 +13293,9 @@
 			}
 		},
 		"lerna": {
-			"version": "3.22.1",
-			"resolved": "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz",
-			"integrity": "sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg==",
+			"version": "3.22.0",
+			"resolved": "https://registry.npmjs.org/lerna/-/lerna-3.22.0.tgz",
+			"integrity": "sha512-xWlHdAStcqK/IjKvjsSMHPZjPkBV1lS60PmsIeObU8rLljTepc4Sg/hncw4HWfQxPIewHAUTqhrxPIsqf9L2Eg==",
 			"dev": true,
 			"requires": {
 				"@lerna/add": "3.21.0",
@@ -13311,9 +13311,9 @@
 				"@lerna/init": "3.21.0",
 				"@lerna/link": "3.21.0",
 				"@lerna/list": "3.21.0",
-				"@lerna/publish": "3.22.1",
+				"@lerna/publish": "3.22.0",
 				"@lerna/run": "3.21.0",
-				"@lerna/version": "3.22.1",
+				"@lerna/version": "3.22.0",
 				"import-local": "^2.0.0",
 				"npmlog": "^4.1.2"
 			}

+ 2 - 1
package.json

@@ -117,5 +117,6 @@
     "webpack-dev-server": "^3.11.0",
     "webpack-merge": "^4.2.1",
     "xss": "^1.0.6"
-  }
+  },
+  "dependencies": {}
 }

+ 2 - 2
spec/chatbox.js

@@ -906,7 +906,7 @@ describe("Chatboxes", function () {
                 }));
             });
 
-            describe("An inactive notifciation", function () {
+            describe("An inactive notification", function () {
 
                 it("is sent if the user has stopped typing since 2 minutes",
                     mock.initConverse(
@@ -1056,7 +1056,7 @@ describe("Chatboxes", function () {
                 }));
             });
 
-            describe("A gone notifciation", function () {
+            describe("A gone notification", function () {
 
                 it("will be shown if received",
                     mock.initConverse(

+ 22 - 18
spec/muc_messages.js

@@ -1024,15 +1024,15 @@ describe("A Groupchat Message", function () {
             [text, references] = view.model.parseTextForReferences('hello @z3r0')
             expect(references.length).toBe(1);
             expect(text).toBe('hello z3r0');
-            expect(JSON.stringify(references))
-                .toBe('[{"begin":6,"end":10,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"}]');
+            expect(references)
+                .toEqual([{"begin":6,"end":10,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"}]);
 
             [text, references] = view.model.parseTextForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?')
             expect(text).toBe('hello @some1 z3r0 gibson mr.robot, how are you?');
-            expect(JSON.stringify(references))
-                .toBe('[{"begin":13,"end":17,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"},'+
-                        '{"begin":18,"end":24,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"},'+
-                        '{"begin":25,"end":33,"value":"mr.robot","type":"mention","uri":"xmpp:mr.robot@montague.lit"}]');
+            expect(references)
+                .toEqual([{"begin":13,"end":17,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"},
+                        {"begin":18,"end":24,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"},
+                        {"begin":25,"end":33,"value":"mr.robot","type":"mention","uri":"xmpp:mr.robot@montague.lit"}]);
 
             [text, references] = view.model.parseTextForReferences('yo @gib')
             expect(text).toBe('yo @gib');
@@ -1042,43 +1042,47 @@ describe("A Groupchat Message", function () {
             expect(text).toBe('yo @gibsonian');
             expect(references.length).toBe(0);
 
+            [text, references] = view.model.parseTextForReferences('yo @GiBsOn')
+            expect(text).toBe('yo gibson');
+            expect(references.length).toBe(1);
+
             [text, references] = view.model.parseTextForReferences('@gibson')
             expect(text).toBe('gibson');
             expect(references.length).toBe(1);
-            expect(JSON.stringify(references))
-                .toBe('[{"begin":0,"end":6,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"}]');
+            expect(references)
+                .toEqual([{"begin":0,"end":6,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"}]);
 
             [text, references] = view.model.parseTextForReferences('hi @Link Mauve how are you?')
             expect(text).toBe('hi Link Mauve how are you?');
             expect(references.length).toBe(1);
-            expect(JSON.stringify(references))
-                .toBe('[{"begin":3,"end":13,"value":"Link Mauve","type":"mention","uri":"xmpp:Link-Mauve@montague.lit"}]');
+            expect(references)
+                .toEqual([{"begin":3,"end":13,"value":"Link Mauve","type":"mention","uri":"xmpp:Link-Mauve@montague.lit"}]);
 
             [text, references] = view.model.parseTextForReferences('https://example.org/@gibson')
             expect(text).toBe('https://example.org/@gibson');
             expect(references.length).toBe(0);
-            expect(JSON.stringify(references))
-                .toBe('[]');
+            expect(references)
+                .toEqual([]);
 
             [text, references] = view.model.parseTextForReferences('mail@gibson.com')
             expect(text).toBe('mail@gibson.com');
             expect(references.length).toBe(0);
-            expect(JSON.stringify(references))
-                .toBe('[]');
+            expect(references)
+                .toEqual([]);
 
             [text, references] = view.model.parseTextForReferences(
                 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr')
             expect(text).toBe(
                 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr');
             expect(references.length).toBe(0);
-            expect(JSON.stringify(references))
-                .toBe('[]');
+            expect(references)
+                .toEqual([]);
 
             [text, references] = view.model.parseTextForReferences('@gh0st where are you?')
             expect(text).toBe('gh0st where are you?');
             expect(references.length).toBe(1);
-            expect(JSON.stringify(references))
-                .toBe('[{"begin":0,"end":5,"value":"gh0st","type":"mention","uri":"xmpp:lounge@montague.lit/gh0st"}]');
+            expect(references)
+                .toEqual([{"begin":0,"end":5,"value":"gh0st","type":"mention","uri":"xmpp:lounge@montague.lit/gh0st"}]);
             done();
         }));
 

+ 52 - 64
src/headless/converse-muc.js

@@ -15,6 +15,7 @@ import log from "./log";
 import muc_utils from "./utils/muc";
 import st from "./utils/stanza";
 import u from "./utils/form";
+import p from "./utils/parse-helpers";
 
 export const ROLES = ['moderator', 'participant', 'visitor'];
 export const AFFILIATIONS = ['owner', 'admin', 'member', 'outcast', 'none'];
@@ -941,74 +942,61 @@ converse.plugins.add('converse-muc', {
                 ])].filter(n => n);
             },
 
-            getReferenceForMention (mention, index) {
-                const nicknames = this.getAllKnownNicknames();
-                const longest_match = u.getLongestSubstring(mention, nicknames);
-                if (!longest_match) {
-                    return null;
-                }
-                if ((mention[longest_match.length] || '').match(/[A-Za-zäëïöüâêîôûáéíóúàèìòùÄËÏÖÜÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙ0-9]/i)) {
-                    // avoid false positives, i.e. mentions that have
-                    // further alphabetical characters than our longest
-                    // match.
-                    return null;
-                }
-
-                let uri;
-                const occupant = this.occupants.findOccupant({'nick': longest_match}) ||
-                        u.isValidJID(longest_match) && this.occupants.findOccupant({'jid': longest_match});
+            getAllKnownNicknamesRegex () {
+                const longNickString = this.getAllKnownNicknames().join('|');
+                const escapedLongNickString = p.escapeRegexString(longNickString)
+                return RegExp(`(?:\\s|^)@(${escapedLongNickString})(?![\\w@-])`, 'ig');
+            },
 
-                if (occupant) {
-                    uri = occupant.get('jid') || `${this.get('jid')}/${occupant.get('nick')}`;
-                } else if (nicknames.includes(longest_match)) {
-                    // TODO: show a warning to the user that the person is not currently in the chat
-                    uri = `${this.get('jid')}/${longest_match}`;
-                } else {
-                    return;
-                }
-                const obj = {
-                    'begin': index,
-                    'end': index + longest_match.length,
-                    'value': longest_match,
-                    'type': 'mention',
-                    'uri': encodeURI(`xmpp:${uri}`)
-                };
-                return obj;
+            getOccupantByJID (jid) {
+                return this.occupants.findOccupant({ jid });
             },
 
-            extractReference (text, index) {
-                for (let i=index; i<text.length; i++) {
-                    if (text[i] === '@' && (i === 0 || text[i - 1] === ' ')) {
-                        const match = text.slice(i+1),
-                              ref = this.getReferenceForMention(match, i);
-                        if (ref) {
-                            return [text.slice(0, i) + match, ref, i]
-                        }
-                    }
-                }
-                return;
+            getOccupantByNickname (nick) {
+                return this.occupants.findOccupant({ nick });
             },
 
-            parseTextForReferences (text) {
-                const refs = [];
-                let index = 0;
-                while (index < (text || '').length) {
-                    const result = this.extractReference(text, index);
-                    if (result) {
-                        text = result[0]; // @ gets filtered out
-                        refs.push(result[1]);
-                        index = result[2];
-                    } else {
-                        break;
-                    }
+            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, []];
+
+                const known_nicknames = this.getAllKnownNicknames();
+                const known_nicknames_with_at_regex = this.getAllKnownNicknamesRegex();
+                const getMatchesForNickRegex = nick_regex => [...findRegexInMessage(nick_regex)];
+                const getNicknameFromRegex = p.findFirstMatchInArray(known_nicknames);
+
+                const uriFromNickname = nickname => {
+                    const jid = this.get('jid');
+                    const occupant  = this.getOccupant(nickname) || this.getOccupant(jid);
+                    const uri = (occupant && occupant.get('jid')) || `${jid}/${nickname}`;
+                    return encodeURI(`xmpp:${uri}`);
+                };
+
+                const matchToReference = match => {
+                    const at_sign_index = match[0].indexOf('@');
+                    const begin = match.index + at_sign_index;
+                    const end = begin + match[0].length - at_sign_index;
+                    const value = getNicknameFromRegex(RegExp(match[1], 'i'));
+                    const type = 'mention';
+                    const uri = uriFromNickname(value);
+                    return { begin, end, value, type, uri }
                 }
-                return [text, refs];
+
+                const mentions = getMatchesForNickRegex(known_nicknames_with_at_regex);
+                const references = mentions.map(matchToReference);
+
+                const [updated_message, updated_references] = p.reduceTextFromReferences(
+                    original_message,
+                    references
+                );
+                return [updated_message, updated_references];
             },
 
-            getOutgoingMessageAttributes (text, spoiler_hint) {
+            getOutgoingMessageAttributes (original_message, spoiler_hint) {
                 const is_spoiler = this.get('composing_spoiler');
-                var references;
-                [text, references] = this.parseTextForReferences(text);
+                const [text, references] = this.parseTextForReferences(original_message);
                 const origin_id = u.getUniqueId();
                 const body = text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined;
                 return {
@@ -1393,13 +1381,13 @@ converse.plugins.add('converse-muc', {
             /**
              * @private
              * @method _converse.ChatRoom#getOccupant
-             * @param { String } nick_or_jid - The nickname or JID of the occupant to be returned
+             * @param { String } nickname_or_jid - The nickname or JID of the occupant to be returned
              * @returns { _converse.ChatRoomOccupant }
              */
-            getOccupant (nick_or_jid) {
-                return (u.isValidJID(nick_or_jid) &&
-                    this.occupants.findWhere({'jid': nick_or_jid})) ||
-                    this.occupants.findWhere({'nick': nick_or_jid});
+            getOccupant (nickname_or_jid) {
+                return u.isValidJID(nickname_or_jid)
+                    ? this.getOccupantByJID(nickname_or_jid)
+                    : this.getOccupantByNickname(nickname_or_jid);
             },
 
             /**

+ 43 - 0
src/headless/utils/parse-helpers.js

@@ -0,0 +1,43 @@
+/**
+ * @copyright 2020, the Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ * @description Pure functions to help funcitonally parse messages.
+ * @todo Other parsing helpers can be made more abstract and placed here.
+ */
+const helpers = {};
+
+// Captures all mentions, but includes a space before the @
+helpers.mention_regex = /(?:\s|^)([@][\w_-]+(?:\.\w+)*)/ig;
+
+helpers.matchRegexInText = text => regex => text.matchAll(regex);
+
+const escapeRegexChars = (string, char) => string.replace(RegExp('\\' + char, 'ig'), '\\' + char);
+
+helpers.escapeCharacters = characters => string =>
+    characters.split('').reduce(escapeRegexChars, string);
+
+helpers.escapeRegexString = helpers.escapeCharacters('[\\^$.?*+(){}');
+
+// `for` is ~25% faster than using `Array.find()`
+helpers.findFirstMatchInArray = array => regex => {
+    for (let i = 0; i < array.length; i++) {
+        if (regex.test(array[i])) {
+            return array[i];
+        }
+    }
+    return null;
+};
+
+const reduceReferences = ([text, refs], ref, index) => {
+    let updated_text = text;
+    let { begin, end } = ref;
+    const { value } = ref;
+    begin = begin - index;
+    end = end - index - 1; // -1 to compensate for the removed @
+    updated_text = `${updated_text.slice(0, begin)}${value}${updated_text.slice(end + 1)}`;
+    return [updated_text, [...refs, { ...ref, begin, end }]]
+}
+
+helpers.reduceTextFromReferences = (text, refs) => refs.reduce(reduceReferences, [text, []]);
+
+export default helpers;