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/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/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?.(); 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"