frontend-rw #1
2 changed files with 77 additions and 79 deletions
|
@ -1,6 +1,6 @@
|
||||||
import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser';
|
import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
|
@ -26,98 +26,96 @@ interface IParsedContent {
|
||||||
emojis?: Array<CustomEmoji>;
|
emojis?: Array<CustomEmoji>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote, emojis }) => {
|
const ParsedContent: React.FC<IParsedContent> = React.memo(({ html, mentions, hasQuote, emojis }) => {
|
||||||
return useMemo(() => {
|
if (html.length === 0) {
|
||||||
if (html.length === 0) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const emojiMap = emojis ? makeEmojiMap(emojis) : undefined;
|
const emojiMap = emojis ? makeEmojiMap(emojis) : undefined;
|
||||||
|
|
||||||
const selectors: Array<string> = [];
|
const selectors: Array<string> = [];
|
||||||
|
|
||||||
// Explicit mentions
|
// Explicit mentions
|
||||||
if (mentions) selectors.push('recipients-inline');
|
if (mentions) selectors.push('recipients-inline');
|
||||||
|
|
||||||
// Quote posting
|
// Quote posting
|
||||||
if (hasQuote) selectors.push('quote-inline');
|
if (hasQuote) selectors.push('quote-inline');
|
||||||
|
|
||||||
const options: HTMLReactParserOptions = {
|
const options: HTMLReactParserOptions = {
|
||||||
replace(domNode) {
|
replace(domNode) {
|
||||||
if (!(domNode instanceof Element)) {
|
if (!(domNode instanceof Element)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['script', 'iframe'].includes(domNode.name)) {
|
if (['script', 'iframe'].includes(domNode.name)) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domNode.attribs.class?.split(' ').some(className => selectors.includes(className))) {
|
if (domNode.attribs.class?.split(' ').some(className => selectors.includes(className))) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domNode.name === 'a') {
|
if (domNode.name === 'a') {
|
||||||
const classes = domNode.attribs.class?.split(' ');
|
const classes = domNode.attribs.class?.split(' ');
|
||||||
|
|
||||||
const fallback = (
|
const fallback = (
|
||||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||||
<a
|
<a
|
||||||
{...domNode.attribs}
|
{...domNode.attribs}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
rel='nofollow noopener'
|
rel='nofollow noopener'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
title={domNode.attribs.href}
|
title={domNode.attribs.href}
|
||||||
>
|
>
|
||||||
{domToReact(domNode.children as DOMNode[], options)}
|
{domToReact(domNode.children as DOMNode[], options)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (classes?.includes('mention')) {
|
if (classes?.includes('mention')) {
|
||||||
if (mentions) {
|
if (mentions) {
|
||||||
const mention = mentions.find(({ url }) => domNode.attribs.href === url);
|
const mention = mentions.find(({ url }) => domNode.attribs.href === url);
|
||||||
if (mention) {
|
if (mention) {
|
||||||
return (
|
|
||||||
<HoverAccountWrapper accountId={mention.id} element='span'>
|
|
||||||
<Link
|
|
||||||
to={`/@${mention.acct}`}
|
|
||||||
className='text-primary-600 hover:underline dark:text-accent-blue'
|
|
||||||
dir='ltr'
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
@{mention.username}
|
|
||||||
</Link>
|
|
||||||
</HoverAccountWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (domNode.attribs['data-user']) {
|
|
||||||
return (
|
return (
|
||||||
<StatusMention accountId={domNode.attribs['data-user']} fallback={fallback} />
|
<HoverAccountWrapper accountId={mention.id} element='span'>
|
||||||
|
<Link
|
||||||
|
to={`/@${mention.acct}`}
|
||||||
|
className='text-primary-600 hover:underline dark:text-accent-blue'
|
||||||
|
dir='ltr'
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
@{mention.username}
|
||||||
|
</Link>
|
||||||
|
</HoverAccountWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (domNode.attribs['data-user']) {
|
||||||
|
return (
|
||||||
|
<StatusMention accountId={domNode.attribs['data-user']} fallback={fallback} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classes?.includes('hashtag')) {
|
|
||||||
const hashtag = nodesToText(domNode.children as Array<DOMNode>);
|
|
||||||
if (hashtag) {
|
|
||||||
return <HashtagLink hashtag={hashtag.replace(/^#/, '')} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
transform(reactNode, _domNode, index) {
|
|
||||||
if (typeof reactNode === 'string') {
|
|
||||||
return <Emojify key={index} text={reactNode} emojis={emojiMap} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return reactNode as JSX.Element;
|
if (classes?.includes('hashtag')) {
|
||||||
},
|
const hashtag = nodesToText(domNode.children as Array<DOMNode>);
|
||||||
};
|
if (hashtag) {
|
||||||
|
return <HashtagLink hashtag={hashtag.replace(/^#/, '')} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
return fallback;
|
||||||
}, [html]);
|
}
|
||||||
});
|
},
|
||||||
|
|
||||||
|
transform(reactNode, _domNode, index) {
|
||||||
|
if (typeof reactNode === 'string') {
|
||||||
|
return <Emojify key={index} text={reactNode} emojis={emojiMap} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactNode as JSX.Element;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
||||||
|
}, (prevProps, nextProps) => prevProps.html === nextProps.html);
|
||||||
|
|
||||||
export { ParsedContent };
|
export { ParsedContent };
|
||||||
|
|
|
@ -33,7 +33,7 @@ interface IEmojify {
|
||||||
emojis?: Array<CustomEmoji> | Record<string, CustomEmoji>;
|
emojis?: Array<CustomEmoji> | Record<string, CustomEmoji>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Emojify: React.FC<IEmojify> = ({ text, emojis = {} }) => React.useMemo(() => {
|
const Emojify: React.FC<IEmojify> = React.memo(({ text, emojis = {} }) => {
|
||||||
if (Array.isArray(emojis)) emojis = makeEmojiMap(emojis);
|
if (Array.isArray(emojis)) emojis = makeEmojiMap(emojis);
|
||||||
|
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
|
@ -102,6 +102,6 @@ const Emojify: React.FC<IEmojify> = ({ text, emojis = {} }) => React.useMemo(()
|
||||||
if (stack.length) nodes.push(stack);
|
if (stack.length) nodes.push(stack);
|
||||||
|
|
||||||
return nodes;
|
return nodes;
|
||||||
}, [text, emojis]);
|
});
|
||||||
|
|
||||||
export { Emojify as default };
|
export { Emojify as default };
|
||||||
|
|
Loading…
Reference in a new issue