|
1 | 1 | import React from 'react'; |
2 | 2 | import ReactMarkdown, { defaultUrlTransform } from 'react-markdown'; |
3 | 3 | import { find } from 'linkifyjs'; |
4 | | -import uniqBy from 'lodash.uniqby'; |
5 | 4 | import remarkGfm from 'remark-gfm'; |
6 | 5 | import type { ComponentType } from 'react'; |
7 | 6 | import type { Options } from 'react-markdown/lib'; |
8 | 7 | import type { UserResponse } from 'stream-chat'; |
9 | | -import type { PluggableList } from 'unified'; // A subdependency of react-markdown. The type is not declared or re-exported from anywhere else |
| 8 | +import type { PluggableList } from 'unified'; // A sub-dependency of react-markdown. The type is not declared or re-exported from anywhere else |
10 | 9 |
|
11 | 10 | import { Anchor, Emoji, Mention } from './componentRenderers'; |
12 | | -import { detectHttp, escapeRegExp, matchMarkdownLinks, messageCodeBlocks } from './regex'; |
| 11 | +import { detectHttp, matchMarkdownLinks, messageCodeBlocks } from './regex'; |
13 | 12 | import { emojiMarkdownPlugin, mentionsMarkdownPlugin } from './rehypePlugins'; |
14 | 13 | import { htmlToTextPlugin, keepLineBreaksPlugin } from './remarkPlugins'; |
15 | 14 | import { ErrorBoundary } from '../../UtilityComponents'; |
@@ -108,60 +107,57 @@ export const renderText = ( |
108 | 107 | const markdownLinks = matchMarkdownLinks(newText); |
109 | 108 | const codeBlocks = messageCodeBlocks(newText); |
110 | 109 |
|
111 | | - // extract all valid links/emails within text and replace it with proper markup |
112 | | - uniqBy([...find(newText, 'email'), ...find(newText, 'url')], 'value').forEach( |
113 | | - ({ href, type, value }) => { |
114 | | - const linkIsInBlock = codeBlocks.some((block) => block?.includes(value)); |
115 | | - |
116 | | - // check if message is already markdown |
117 | | - const noParsingNeeded = |
118 | | - markdownLinks && |
119 | | - markdownLinks.filter((text) => { |
120 | | - const strippedHref = href?.replace(detectHttp, ''); |
121 | | - const strippedText = text?.replace(detectHttp, ''); |
122 | | - |
123 | | - if (!strippedHref || !strippedText) return false; |
124 | | - |
125 | | - return ( |
126 | | - strippedHref.includes(strippedText) || strippedText.includes(strippedHref) |
127 | | - ); |
128 | | - }); |
129 | | - |
130 | | - if (noParsingNeeded.length > 0 || linkIsInBlock) return; |
131 | | - |
132 | | - try { |
133 | | - // special case for mentions: |
134 | | - // it could happen that a user's name matches with an e-mail format pattern. |
135 | | - // in that case, we check whether the found e-mail is actually a mention |
136 | | - // by naively checking for an existence of @ sign in front of it. |
137 | | - if (type === 'email' && mentionedUsers) { |
138 | | - const emailMatchesWithName = mentionedUsers.some((u) => u.name === value); |
139 | | - if (emailMatchesWithName) { |
140 | | - newText = newText.replace( |
141 | | - new RegExp(escapeRegExp(value), 'g'), |
142 | | - (match, position) => { |
143 | | - const isMention = newText.charAt(position - 1) === '@'; |
144 | | - // in case of mention, we leave the match in its original form, |
145 | | - // and we let `mentionsMarkdownPlugin` to do its job |
146 | | - return isMention ? match : `[${match}](${encodeDecode(href)})`; |
147 | | - }, |
148 | | - ); |
149 | | - |
150 | | - return; |
151 | | - } |
| 110 | + // Extract all valid links/emails within text and replace it with proper markup |
| 111 | + // Revert the link order to avoid getting out of sync of the original start and end positions of links |
| 112 | + // - due to the addition of new characters when creating Markdown links |
| 113 | + const links = [...find(newText, 'email'), ...find(newText, 'url')]; |
| 114 | + for (let i = links.length - 1; i >= 0; i--) { |
| 115 | + const { end, href, start, type, value } = links[i]; |
| 116 | + const linkIsInBlock = codeBlocks.some((block) => block?.includes(value)); |
| 117 | + |
| 118 | + // check if message is already markdown |
| 119 | + const noParsingNeeded = |
| 120 | + markdownLinks && |
| 121 | + markdownLinks.filter((text) => { |
| 122 | + const strippedHref = href?.replace(detectHttp, ''); |
| 123 | + const strippedText = text?.replace(detectHttp, ''); |
| 124 | + |
| 125 | + if (!strippedHref || !strippedText) return false; |
| 126 | + |
| 127 | + return strippedHref.includes(strippedText) || strippedText.includes(strippedHref); |
| 128 | + }); |
| 129 | + |
| 130 | + if (noParsingNeeded.length > 0 || linkIsInBlock) return; |
| 131 | + |
| 132 | + try { |
| 133 | + // special case for mentions: |
| 134 | + // it could happen that a user's name matches with an e-mail format pattern. |
| 135 | + // in that case, we check whether the found e-mail is actually a mention |
| 136 | + // by naively checking for an existence of @ sign in front of it. |
| 137 | + if (type === 'email' && mentionedUsers) { |
| 138 | + const emailMatchesWithName = mentionedUsers.find((u) => u.name === value); |
| 139 | + if (emailMatchesWithName) { |
| 140 | + // FIXME: breaks if the mention symbol is not '@' |
| 141 | + const isMention = newText.charAt(start - 1) === '@'; |
| 142 | + // in case of mention, we leave the match in its original form, |
| 143 | + // and we let `mentionsMarkdownPlugin` to do its job |
| 144 | + newText = |
| 145 | + newText.slice(0, start) + |
| 146 | + (isMention ? value : `[${value}](${encodeDecode(href)})`) + |
| 147 | + newText.slice(end); |
152 | 148 | } |
153 | | - |
| 149 | + } else { |
154 | 150 | const displayLink = type === 'email' ? value : formatUrlForDisplay(href); |
155 | 151 |
|
156 | | - newText = newText.replace( |
157 | | - new RegExp(escapeRegExp(value), 'g'), |
158 | | - `[${displayLink}](${encodeDecode(href)})`, |
159 | | - ); |
160 | | - } catch (e) { |
161 | | - void e; |
| 152 | + newText = |
| 153 | + newText.slice(0, start) + |
| 154 | + `[${displayLink}](${encodeDecode(href)})` + |
| 155 | + newText.slice(end); |
162 | 156 | } |
163 | | - }, |
164 | | - ); |
| 157 | + } catch (e) { |
| 158 | + void e; |
| 159 | + } |
| 160 | + } |
165 | 161 |
|
166 | 162 | const remarkPlugins: PluggableList = [ |
167 | 163 | htmlToTextPlugin, |
|
0 commit comments