From 63924bcc50a626deac63491b49867e1c49d3f36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 3 Oct 2024 12:09:35 +0200 Subject: [PATCH] pl-fe: Move parsed html to a separate component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../pl-fe/src/components/parsed-content.tsx | 80 +++++++++++++++++++ .../pl-fe/src/components/status-content.tsx | 76 ++---------------- .../compose/components/reply-indicator.tsx | 8 +- 3 files changed, 91 insertions(+), 73 deletions(-) create mode 100644 packages/pl-fe/src/components/parsed-content.tsx diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx new file mode 100644 index 000000000..6c3a65625 --- /dev/null +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -0,0 +1,80 @@ +import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser'; +import React, { useMemo } from 'react'; +import { Link } from 'react-router-dom'; + +import HashtagLink from './hashtag-link'; +import HoverRefWrapper from './hover-ref-wrapper'; + +import type { Mention } from 'pl-api'; + +const nodesToText = (nodes: Array): string => + nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array) : '').join(''); + +interface IParsedContent { + html: string; + mentions?: Array; +} + +const ParsedContent: React.FC = (({ html, mentions }) => { + return useMemo(() => { + if (html.length === 0) { + return null; + } + + const options: HTMLReactParserOptions = { + replace(domNode) { + if (domNode instanceof Element && ['script', 'iframe'].includes(domNode.name)) { + return null; + } + + if (domNode instanceof Element && domNode.name === 'a') { + const classes = domNode.attribs.class?.split(' '); + + const fallback = ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + e.stopPropagation()} + rel='nofollow noopener' + target='_blank' + title={domNode.attribs.href} + > + {domToReact(domNode.children as DOMNode[], options)} + + ); + + if (classes?.includes('mention') && mentions) { + const mention = mentions.find(({ url }) => domNode.attribs.href === url); + if (mention) { + return ( + + e.stopPropagation()} + > + @{mention.username} + + + ); + } + } + + if (classes?.includes('hashtag')) { + const hashtag = nodesToText(domNode.children as Array); + if (hashtag) { + return ; + } + } + + return fallback; + } + }, + }; + + return parse(html, options); + }, [html]); +}); + +export { ParsedContent }; diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index 4def4b085..4c2e498a2 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -1,8 +1,6 @@ import clsx from 'clsx'; -import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser'; import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router-dom'; import { collapseStatusSpoiler, expandStatusSpoiler } from 'pl-fe/actions/statuses'; import Icon from 'pl-fe/components/icon'; @@ -12,9 +10,8 @@ import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content'; import { getTextDirection } from '../utils/rtl'; -import HashtagLink from './hashtag-link'; -import HoverRefWrapper from './hover-ref-wrapper'; import Markup from './markup'; +import { ParsedContent } from './parsed-content'; import Poll from './polls/poll'; import type { Sizes } from 'pl-fe/components/ui/text/text'; @@ -22,9 +19,6 @@ import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; const BIG_EMOJI_LIMIT = 10; -const nodesToText = (nodes: Array): string => - nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array) : '').join(''); - interface IReadMoreButton { onClick: React.MouseEventHandler; quote?: boolean; @@ -118,64 +112,6 @@ const StatusContent: React.FC = React.memo(({ [status.contentHtml, status.translation, status.currentLanguage], ); - const content = useMemo(() => { - if (status.content.length === 0) { - return null; - } - - const options: HTMLReactParserOptions = { - replace(domNode) { - if (domNode instanceof Element && ['script', 'iframe'].includes(domNode.name)) { - return null; - } - - if (domNode instanceof Element && domNode.name === 'a') { - const classes = domNode.attribs.class?.split(' '); - - if (classes?.includes('mention')) { - const mention = status.mentions.find(({ url }) => domNode.attribs.href === url); - if (mention) { - return ( - - e.stopPropagation()} - > - @{mention.username} - - - ); - } - } - - if (classes?.includes('hashtag')) { - const hashtag = nodesToText(domNode.children as Array); - if (hashtag) { - return ; - } - } - - return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions - e.stopPropagation()} - rel='nofollow noopener' - target='_blank' - title={domNode.attribs.href} - > - {domToReact(domNode.children as DOMNode[], options)} - - ); - } - }, - }; - - return parse(parsedHtml, options); - }, [parsedHtml]); - useEffect(() => { setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96); }, [spoilerNode.current]); @@ -208,7 +144,7 @@ const StatusContent: React.FC = React.memo(({ dangerouslySetInnerHTML={{ __html: spoilerText }} ref={spoilerNode} /> - {content && expandable && ( + {status.content && expandable && (