UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

132 lines (131 loc) 5.22 kB
import React from 'react'; import ReactMarkdown, { defaultUrlTransform } from 'react-markdown'; import { find } from 'linkifyjs'; import remarkGfm from 'remark-gfm'; import { Anchor, Emoji, Mention } from './componentRenderers'; import { detectHttp, matchMarkdownLinks, messageCodeBlocks } from './regex'; import { emojiMarkdownPlugin, mentionsMarkdownPlugin } from './rehypePlugins'; import { htmlToTextPlugin, keepLineBreaksPlugin } from './remarkPlugins'; import { ErrorBoundary } from '../../UtilityComponents'; export const defaultAllowedTagNames = [ 'html', 'text', 'br', 'p', 'em', 'strong', 'a', 'ol', 'ul', 'li', 'code', 'pre', 'blockquote', 'del', 'table', 'thead', 'tbody', 'th', 'tr', 'td', 'tfoot', // custom types (tagNames) 'emoji', 'mention', ]; function formatUrlForDisplay(url) { try { return decodeURIComponent(url).replace(detectHttp, ''); } catch (e) { return url; } } function encodeDecode(url) { try { return encodeURI(decodeURIComponent(url)); } catch (error) { return url; } } const urlTransform = (uri) => uri.startsWith('app://') ? uri : defaultUrlTransform(uri); const getPluginsForward = (plugins) => plugins; export const markDownRenderers = { a: Anchor, emoji: Emoji, mention: Mention, }; export const renderText = (text, mentionedUsers, { allowedTagNames = defaultAllowedTagNames, customMarkDownRenderers, getRehypePlugins = getPluginsForward, getRemarkPlugins = getPluginsForward, } = {}) => { // take the @ mentions and turn them into markdown? // translate links if (!text) return null; if (text.trim().length === 1) return React.createElement(React.Fragment, null, text); let newText = text; const markdownLinks = matchMarkdownLinks(newText); const codeBlocks = messageCodeBlocks(newText); // Extract all valid links/emails within text and replace it with proper markup // Revert the link order to avoid getting out of sync of the original start and end positions of links // - due to the addition of new characters when creating Markdown links const links = [...find(newText, 'email'), ...find(newText, 'url')]; for (let i = links.length - 1; i >= 0; i--) { const { end, href, start, type, value } = links[i]; const linkIsInBlock = codeBlocks.some((block) => block?.includes(value)); // check if message is already markdown const noParsingNeeded = markdownLinks && markdownLinks.filter((text) => { const strippedHref = href?.replace(detectHttp, ''); const strippedText = text?.replace(detectHttp, ''); if (!strippedHref || !strippedText) return false; return strippedHref.includes(strippedText) || strippedText.includes(strippedHref); }); if (noParsingNeeded.length > 0 || linkIsInBlock) continue; 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' && mentionedUsers) { const emailMatchesWithName = mentionedUsers.find((u) => u.name === value); if (emailMatchesWithName) { // FIXME: breaks if the mention symbol is not '@' const isMention = newText.charAt(start - 1) === '@'; // in case of mention, we leave the match in its original form, // and we let `mentionsMarkdownPlugin` to do its job newText = newText.slice(0, start) + (isMention ? value : `[${value}](${encodeDecode(href)})`) + newText.slice(end); } } else { const displayLink = type === 'email' ? value : formatUrlForDisplay(href); newText = newText.slice(0, start) + `[${displayLink}](${encodeDecode(href)})` + newText.slice(end); } } catch (e) { void e; } } const remarkPlugins = [ htmlToTextPlugin, keepLineBreaksPlugin, [remarkGfm, { singleTilde: false }], ]; const rehypePlugins = [emojiMarkdownPlugin]; if (mentionedUsers?.length) { rehypePlugins.push(mentionsMarkdownPlugin(mentionedUsers)); } return (React.createElement(ErrorBoundary, { fallback: React.createElement(React.Fragment, null, text) }, React.createElement(ReactMarkdown, { allowedElements: allowedTagNames, components: { ...markDownRenderers, ...customMarkDownRenderers, }, rehypePlugins: getRehypePlugins(rehypePlugins), remarkPlugins: getRemarkPlugins(remarkPlugins), skipHtml: true, unwrapDisallowed: true, urlTransform: urlTransform }, newText))); };