stream-chat-react
Version:
React components to create chat conversations or livestream style chat
132 lines (131 loc) • 5.22 kB
JavaScript
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)));
};