diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 9662014a..42bde3d2 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -724,3 +724,145 @@ test('Test quotes markdown replacement with heading inside', () => { testString = '> # heading A\n> # heading B'; expect(parser.replace(testString)).toBe('

heading A

heading B

'); }); + +// Valid text that should match for user mentions +test('Test for user mention with @username@domain.com', () => { + const testString = '@username@expensify.com'; + const resultString = '@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with @phoneNumber@domain.sms', () => { + const testString = '@+19728974297@expensify.sms'; + const resultString = '@+19728974297@expensify.sms'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with bold style', () => { + const testString = '*@username@expensify.com*'; + const resultString = '@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with italic style', () => { + const testString = '_@username@expensify.com_'; + const resultString = '@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with heading1 style', () => { + const testString = '# @username@expensify.com'; + const resultString = '

@username@expensify.com

'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with strikethrough style', () => { + const testString = '~@username@expensify.com~'; + const resultString = '@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with @here', () => { + const testString = '@here say hello to @newuser@expensify.com'; + const resultString = '@here say hello to @newuser@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +// Invalid text should not match for user mentions: +test('Test for user mention without leading whitespace', () => { + const testString = 'hi...@username@expensify.com'; + const resultString = 'hi...@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with @username@expensify', () => { + const testString = '@username@expensify'; + const resultString = '@username@expensify'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with valid email in the middle of a word', () => { + const testString = 'hello username@expensify.com is my email'; + const resultString = 'hello username@expensify.com is my email'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with invalid username', () => { + const testString = '@ +19728974297 hey'; + const resultString = '@ +19728974297 hey'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with codefence style', () => { + const testString = '```@username@expensify.com```'; + const resultString = '
@username@expensify.com
'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention with inlineCodeBlock style', () => { + const testString = '`@username@expensify.com`'; + const resultString = '@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention without space or supported styling character', () => { + const testString = 'hi@username@expensify.com'; + const resultString = 'hi@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for user mention without space or supported styling character', () => { + const testString = 'hi@here'; + const resultString = 'hi@here'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for @here mention with codefence style', () => { + const testString = '```@here```'; + const resultString = '
@here
'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for @here mention with inlineCodeBlock style', () => { + const testString = '`@here`'; + const resultString = '@here'; + expect(parser.replace(testString)).toBe(resultString); +}); + +// Examples that should match for here mentions: +test('Test for here mention with @here', () => { + const testString = '@here'; + const resultString = '@here'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for here mention with leading word and space', () => { + const testString = 'hi all @here'; + const resultString = 'hi all @here'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for here mention with @here in the middle of a word', () => { + const testString = '@here how are you guys?'; + const resultString = '@here how are you guys?'; + expect(parser.replace(testString)).toBe(resultString); +}); + +// Examples that should not match for here mentions: +test('Test for here mention without leading whitespace', () => { + const testString = 'hi...@here'; + const resultString = 'hi...@here'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for here mention with invalid username', () => { + const testString = '@ here hey'; + const resultString = '@ here hey'; + expect(parser.replace(testString)).toBe(resultString); +}); + +test('Test for @here mention without space or supported styling character', () => { + const testString = 'hi@username@expensify.com'; + const resultString = 'hi@username@expensify.com'; + expect(parser.replace(testString)).toBe(resultString); +}); diff --git a/__tests__/Str-test.js b/__tests__/Str-test.js index 69ae015f..1c843951 100644 --- a/__tests__/Str-test.js +++ b/__tests__/Str-test.js @@ -70,3 +70,17 @@ describe('Str.toBool', () => { expect(Str.toBool(undefined)).toBeFalsy(); }); }); + +describe('Str.isValidMention', () => { + it('Correctly detects a valid mentions ', () => { + expect(Str.isValidMention('@username@expensify.com')).toBeTruthy(); + expect(Str.isValidMention('*@username@expensify.com*')).toBeTruthy(); + expect(Str.isValidMention(' @username@expensify.com')).toBeTruthy(); + expect(Str.isValidMention('~@username@expensify.com~')).toBeTruthy(); + expect(Str.isValidMention('#@username@expensify.com')).toBeTruthy(); + expect(Str.isValidMention('_@username@expensify.com_')).toBeTruthy(); + expect(Str.isValidMention('`@username@expensify.com`')).toBeFalsy(); + expect(Str.isValidMention('\'@username@expensify.com\'')).toBeTruthy(); + expect(Str.isValidMention('"@username@expensify.com"')).toBeTruthy(); + }); +}); diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index f005f6ef..c5311279 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -49,6 +49,39 @@ export default class ExpensiMark { replacement: '$1$2$3', }, + /** + * Apply the hereMention first because the string @here is still a valid mention for the userMention regex. + * This ensures that the hereMention is always considered first, even if it is followed by a valid userMention. + */ + { + name: 'hereMentions', + regex: /((`||
)\s*?)?[`.a-zA-Z]?@here\b/gm,
+                replacement: (match) => {
+                    if (!Str.isValidMention(match)) {
+                        return match;
+                    }
+                    return `${match}`;
+                },
+            },
+
+            /**
+             * This regex matches a valid user mention in a string.
+             * A user mention is a string that starts with the '@' symbol and is followed by a valid user's primary login
+             *
+             * Note: currently we are only allowing mentions in a format of @+19728974297@expensify.sms and @username@example.com
+             * The username can contain any combination of alphanumeric letters, numbers, and underscores
+             */
+            {
+                name: 'userMentions',
+                regex: new RegExp(`((`||
)\\s*?)?[\`.a-zA-Z]?@+${CONST.REG_EXP.EMAIL_PART}`, 'gm'),
+                replacement: (match) => {
+                    if (!Str.isValidMention(match)) {
+                        return match;
+                    }
+                    return `${match}`;
+                },
+            },
+
             /**
              * Converts markdown style links to anchor tags e.g. [Expensify](concierge@expensify.com)
              * We need to convert before the auto email link rule and the manual link rule since it will not try to create a link
diff --git a/lib/str.js b/lib/str.js
index 01bd0268..7cab4f9f 100644
--- a/lib/str.js
+++ b/lib/str.js
@@ -935,6 +935,25 @@ const Str = {
         return CONST.SMS.E164_REGEX.test(phone);
     },
 
+    /**
+     * We validate mentions by checking if it's first character is an allowed character
+     * and by checking that we make sure it isn't inside other tags where mentions aren't allowed.
+     * For example, *@username@expensify.com* is a valid mention because we allow bold styling for it,
+     * but `@username@expensify.com` is not because we do not allow mentions within code
+     *
+     * @param {String} mention
+     * @returns {bool}
+     */
+    isValidMention(mention) {
+        // A valid mention starts with a space, *, _, #, ', ", or @ (with no preceding characters).
+        const startsWithValidChar = /[\s*~_#'"@]/g.test(mention.charAt(0));
+
+        // We don't support mention inside code or codefence styling,
+        // for example using `@username@expensify.com` or ```@username@expensify.com``` will be invalid.
+        const containsInvalidTag = /(|
|`)/g.test(mention);
+        return startsWithValidChar && !containsInvalidTag;
+    },
+
     /**
      * Returns text without our SMS domain
      *