diff --git a/package.json b/package.json index bfdbacfdea..d654419108 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "lodash.isequal": "^4.5.0", "lodash.throttle": "^4.1.1", "lodash.uniqby": "^4.7.0", - "mdast-util-find-and-replace": "1.1.1", + "mdast-util-find-and-replace": "^2.2.1", "nanoid": "^3.3.4", "pretty-bytes": "^5.4.1", "prop-types": "^15.7.2", @@ -103,6 +103,7 @@ "@types/lodash.isequal": "^4.5.5", "@types/lodash.throttle": "^4.1.6", "@types/lodash.uniqby": "^4.7.6", + "@types/mdast": "^3.0.10", "@types/moment": "^2.13.0", "@types/react": "^18.0.8", "@types/react-dom": "^18.0.3", @@ -208,7 +209,8 @@ "e2e-container": "./e2e/scripts/run_in_container.sh" }, "resolutions": { - "ast-types": "^0.14.0" + "ast-types": "^0.14.0", + "@types/unist": "^2.0.6" }, "browserslist": [ ">0.2%", diff --git a/src/__tests__/__snapshots__/utils.test.js.snap b/src/__tests__/__snapshots__/utils.test.js.snap new file mode 100644 index 0000000000..8a80dc0894 --- /dev/null +++ b/src/__tests__/__snapshots__/utils.test.js.snap @@ -0,0 +1,142 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renderText handles the special case where user name matches to an e-mail pattern - 1 1`] = ` +

+ Hello + + @username@email.com + + , is + + username@email.com + + your @primary e-mail? +

+`; + +exports[`renderText handles the special case where user name matches to an e-mail pattern - 2 1`] = ` +

+ + username@email.com + + + + @username@email.com + + is this the right address? +

+`; + +exports[`renderText handles the special case where user name matches to an e-mail pattern - 3 1`] = ` +

+ + @username@email.com + + + + @username@email.com + + + + @username@email.com + + + + @username@email.com + +

+`; + +exports[`renderText handles the special case where user name matches to an e-mail pattern - 4 1`] = ` +

+ + @username@email.com + + + + @username@email.com + + + + username@email.com + + + + @username@email.com + +

+`; + +exports[`renderText renders custom mention 1`] = ` +

+ + @username@email.com + + + + @username@email.com + + + + username@email.com + + + + @username@email.com + +

+`; + +exports[`renderText renders standard markdown text 1`] = ` +

+ Hi, shall we meet on + + Tuesday + + ? +

+`; diff --git a/src/__tests__/utils.test.js b/src/__tests__/utils.test.js new file mode 100644 index 0000000000..2ecf2d43e3 --- /dev/null +++ b/src/__tests__/utils.test.js @@ -0,0 +1,63 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { renderText } from '../utils'; + +describe(`renderText`, () => { + it('handles the special case where user name matches to an e-mail pattern - 1', () => { + const Markdown = renderText( + 'Hello @username@email.com, is username@email.com your @primary e-mail?', + [{ id: 'id-username@email.com', name: 'username@email.com' }], + ); + const tree = renderer.create(Markdown).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('handles the special case where user name matches to an e-mail pattern - 2', () => { + const Markdown = renderText( + 'username@email.com @username@email.com is this the right address?', + [{ id: 'id-username@email.com', name: 'username@email.com' }], + ); + const tree = renderer.create(Markdown).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('handles the special case where user name matches to an e-mail pattern - 3', () => { + const Markdown = renderText( + '@username@email.com @username@email.com @username@email.com @username@email.com', + [{ id: 'id-username@email.com', name: 'username@email.com' }], + ); + const tree = renderer.create(Markdown).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('handles the special case where user name matches to an e-mail pattern - 4', () => { + const Markdown = renderText( + '@username@email.com @username@email.com username@email.com @username@email.com', + [{ id: 'id-username@email.com', name: 'username@email.com' }], + ); + const tree = renderer.create(Markdown).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders custom mention', () => { + const Markdown = renderText( + '@username@email.com @username@email.com username@email.com @username@email.com', + [{ id: 'id-username@email.com', name: 'username@email.com' }], + { + customMarkDownRenderers: { + mention: function MyMention(props) { + return {props.children}; + }, + }, + }, + ); + const tree = renderer.create(Markdown).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders standard markdown text', () => { + const Markdown = renderText('Hi, shall we meet on **Tuesday**?', []); + const tree = renderer.create(Markdown).toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/utils.tsx b/src/utils.tsx index d534aced8b..628dfb4f6b 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -2,14 +2,13 @@ import React, { PropsWithChildren } from 'react'; import emojiRegex from 'emoji-regex'; import * as linkify from 'linkifyjs'; import { nanoid } from 'nanoid'; -//@ts-expect-error -import findAndReplace from 'mdast-util-find-and-replace'; +import { findAndReplace, ReplaceFunction } from 'mdast-util-find-and-replace'; import RootReactMarkdown, { NodeType } from 'react-markdown'; import ReactMarkdown from 'react-markdown/with-html'; import uniqBy from 'lodash.uniqby'; import type { UserResponse } from 'stream-chat'; - +import type { Root } from 'mdast'; import type { DefaultStreamChatGenerics } from './types/types'; export const isOnlyEmojis = (text?: string) => { @@ -119,7 +118,7 @@ export const emojiMarkdownPlugin = () => { } const transform = (markdownAST: T) => { - findAndReplace(markdownAST, emojiRegex(), replace); + findAndReplace(markdownAST as Root, emojiRegex(), replace as ReplaceFunction); return markdownAST; }; @@ -156,7 +155,7 @@ export const mentionsMarkdownPlugin = < mentioned_usernames.map((username) => `@${username}`).join('|'), 'g', ); - findAndReplace(markdownAST, mentionedUsersRegex, replace); + findAndReplace(markdownAST as Root, mentionedUsersRegex, replace as ReplaceFunction); return markdownAST; }; @@ -213,6 +212,24 @@ export const renderText = < if (noParsingNeeded.length > 0 || linkIsInBlock) return; try { + // special case for mentions: + // it could happen that a user's name matches with an e-mail format pattern. + // in that case, we check whether the found e-mail is actually a mention + // by naively checking for an existence of @ sign in front of it. + if (type === 'email' && mentioned_users) { + const emailMatchesWithName = mentioned_users.some((u) => u.name === value); + if (emailMatchesWithName) { + newText = newText.replace(new RegExp(escapeRegExp(value), 'g'), (match, position) => { + const isMention = newText.charAt(position - 1) === '@'; + // in case of mention, we leave the match in its original form, + // and we let `mentionsMarkdownPlugin` to do its job + return isMention ? match : `[${match}](${encodeDecode(href)})`; + }); + + return; + } + } + const displayLink = type === 'email' ? value : formatUrlForDisplay(href); newText = newText.replace( diff --git a/yarn.lock b/yarn.lock index 6c399baafb..221441b40f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3207,10 +3207,10 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== -"@types/mdast@^3.0.0", "@types/mdast@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" - integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw== +"@types/mdast@^3.0.0", "@types/mdast@^3.0.3", "@types/mdast@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" + integrity sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA== dependencies: "@types/unist" "*" @@ -3342,10 +3342,10 @@ resolved "https://registry.yarnpkg.com/@types/textarea-caret/-/textarea-caret-3.0.0.tgz#4c5c5e3de5c59511f93ffe929e5383471b828896" integrity sha512-RNXko6Kl+oQibqxuQZJZ+RgsQAGez4VQxeC4zq+GXjUlHcfjy5EthnsPIQXC5wUSRf5WbyA+4+//mVSc6XwxNw== -"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" - integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== +"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3", "@types/unist@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" + integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== "@types/uuid@^8.3.0": version "8.3.0" @@ -7481,10 +7481,10 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== escodegen@^2.0.0: version "2.0.0" @@ -11846,14 +11846,14 @@ mdast-add-list-metadata@1.0.1: dependencies: unist-util-visit-parents "1.1.2" -mdast-util-find-and-replace@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz#b7db1e873f96f66588c321f1363069abf607d1b5" - integrity sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA== +mdast-util-find-and-replace@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.1.tgz#249901ef43c5f41d6e8a8d446b3b63b17e592d7c" + integrity sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw== dependencies: - escape-string-regexp "^4.0.0" - unist-util-is "^4.0.0" - unist-util-visit-parents "^3.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" mdast-util-from-markdown@^0.8.0: version "0.8.5" @@ -17114,6 +17114,11 @@ unist-util-is@^4.0.0: resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== +unist-util-is@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236" + integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ== + unist-util-remove-position@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz#ec037348b6102c897703eee6d0294ca4755a2020" @@ -17153,6 +17158,14 @@ unist-util-visit-parents@^3.0.0: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" +unist-util-visit-parents@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz#44bbc5d25f2411e7dfc5cecff12de43296aa8521" + integrity sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"