frontend-rw #1

Merged
marcin merged 347 commits from frontend-rw into develop 2024-12-05 15:32:18 -08:00
2 changed files with 77 additions and 79 deletions
Showing only changes of commit 5f4dde2590 - Show all commits

View file

@ -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 };

View file

@ -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 };