From 367b7c4cb30454140ff113e2b0a2671a14d9d276 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 3 Aug 2022 15:43:40 +0200 Subject: [PATCH 1/2] fix: detect mentions of users who have email as their name (#1698) The link detection process kicks in before user mentions are detected. With this change, while detecting links we also consider whether the detected link matches an email identical to the mentioned user's name. As part of this change, mdast-util-find-and-replace is updated to its latest version as the one we were previously using had a bug, and sometimes it could skip matching nodes. --- package.json | 6 +- .../__snapshots__/utils.test.js.snap | 142 ++++++++++++++++++ src/__tests__/utils.test.js | 63 ++++++++ src/utils.tsx | 27 +++- yarn.lock | 51 ++++--- 5 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 src/__tests__/__snapshots__/utils.test.js.snap create mode 100644 src/__tests__/utils.test.js 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" From 05eae28cd04f1605ae3fb1cd5767fa4bbbd067d3 Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Wed, 3 Aug 2022 17:16:23 +0200 Subject: [PATCH 2/2] feat: allow to send custom message data when editing a message (#1696) * feat: allow to send custom message data when updating a message * test: verify that custom message data is sent with new & updated messages * docs: add note describing a possibility to pass customMessagData do handleSubmit --- .../override-submit-handler.mdx | 4 ++ .../__tests__/MessageInput.test.js | 52 +++++++++++++++++++ .../MessageInput/hooks/useSubmitHandler.ts | 1 + 3 files changed, 57 insertions(+) diff --git a/docusaurus/docs/React/custom-code-examples/override-submit-handler.mdx b/docusaurus/docs/React/custom-code-examples/override-submit-handler.mdx index a430d1e645..14dbc8a779 100644 --- a/docusaurus/docs/React/custom-code-examples/override-submit-handler.mdx +++ b/docusaurus/docs/React/custom-code-examples/override-submit-handler.mdx @@ -15,6 +15,10 @@ The `MessageInput` component accepts an `overrideSubmitHandler` prop, which allo conclusion of the underlying `textarea` element's [`handleSubmit`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/hooks/useSubmitHandler.ts) function. +:::note +You do not have to implement your custom submit handler, if the only thing you need is to pass custom message data to the underlying API call. In that case you can use the [`handleSubmit`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/hooks/useSubmitHandler.ts) function from the [`MessageInputContext`](../contexts/message-input-context.mdx). The `handleSubmit` function allows you to pass custom message data through its second parameter `customMessageData`. This applies to sending a new message as well as updating an existing one. In order for this to work, you will have to implement custom message input components and pass them to [`Channel`](../core-components/channel.mdx) props `EditMessageInput` or `Input` respectively. +::: + The `overrideSubmitHandler` function receives two arguments, the message to be sent and the `cid` (channel type prepended to channel id) for the currently active channel. The message object is of the following type: diff --git a/src/components/MessageInput/__tests__/MessageInput.test.js b/src/components/MessageInput/__tests__/MessageInput.test.js index ea448f5de0..4b79245d8e 100644 --- a/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/src/components/MessageInput/__tests__/MessageInput.test.js @@ -15,6 +15,7 @@ import { Channel } from '../../Channel/Channel'; import { MessageActionsBox } from '../../MessageActions'; import { MessageProvider } from '../../../context/MessageContext'; +import { useMessageInputContext } from '../../../context/MessageInputContext'; import { useChatContext } from '../../../context/ChatContext'; import { dispatchMessageDeletedEvent, @@ -664,6 +665,57 @@ function axeNoViolations(container) { await axeNoViolations(container); }); + it('should allow to send custom message data', async () => { + const customMessageData = { customX: 'customX' }; + const CustomInputForm = () => { + const { handleChange, handleSubmit, value } = useMessageInputContext(); + return ( +
+ + +
+ ); + }; + + const messageInputProps = + componentName === 'EditMessageForm' + ? { + messageInputProps: { + message: { + text: `abc`, + }, + }, + } + : {}; + + const renderComponent = makeRenderFn(CustomInputForm); + const { container, submit } = await renderComponent(messageInputProps); + + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: 'Some text', + }, + }); + + await act(() => submit()); + + await waitFor(() => { + const calledMock = componentName === 'EditMessageForm' ? editMock : submitMock; + expect(calledMock).toHaveBeenCalledWith( + expect.stringMatching(/.+:.+/), + expect.objectContaining(customMessageData), + ); + }); + await axeNoViolations(container); + }); + it('Should use overrideSubmitHandler prop if it is defined', async () => { const overrideMock = jest.fn().mockImplementation(() => Promise.resolve()); const customMessageData = undefined; diff --git a/src/components/MessageInput/hooks/useSubmitHandler.ts b/src/components/MessageInput/hooks/useSubmitHandler.ts index f58a2a1754..71913332ba 100644 --- a/src/components/MessageInput/hooks/useSubmitHandler.ts +++ b/src/components/MessageInput/hooks/useSubmitHandler.ts @@ -152,6 +152,7 @@ export const useSubmitHandler = < await editMessage(({ ...message, ...updatedMessage, + ...customMessageData, } as unknown) as UpdatedMessage); clearEditingState?.();