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'; import { Button, Stack, Text } from 'pl-fe/components/ui'; import { useAppDispatch, useSettings } from 'pl-fe/hooks'; 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 Poll from './polls/poll'; import type { Sizes } from 'pl-fe/components/ui/text/text'; import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; const MAX_HEIGHT = 322; // 20px * 16 (+ 2px padding at the top) 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; poll?: boolean; } /** Button to expand a truncated status (due to too much content) */ const ReadMoreButton: React.FC = ({ onClick, quote, poll }) => (
); interface IStatusContent { status: MinifiedStatus; onClick?: () => void; collapsable?: boolean; translatable?: boolean; textSize?: Sizes; quote?: boolean; } /** Renders the text content of a status */ const StatusContent: React.FC = React.memo(({ status, onClick, collapsable = false, translatable, textSize = 'md', quote = false, }) => { const dispatch = useAppDispatch(); const { displaySpoilers } = useSettings(); const [collapsed, setCollapsed] = useState(false); const [onlyEmoji, setOnlyEmoji] = useState(false); const [lineClamp, setLineClamp] = useState(true); const node = useRef(null); const spoilerNode = useRef(null); const maybeSetCollapsed = (): void => { if (!node.current) return; if (collapsable && !collapsed) { if (node.current.clientHeight > MAX_HEIGHT) { setCollapsed(true); } } }; const maybeSetOnlyEmoji = (): void => { if (!node.current) return; const only = isOnlyEmoji(node.current, BIG_EMOJI_LIMIT, true); if (only !== onlyEmoji) { setOnlyEmoji(only); } }; const toggleExpanded: React.MouseEventHandler = (e) => { e.preventDefault(); e.stopPropagation(); if (expanded) dispatch(collapseStatusSpoiler(status.id)); else dispatch(expandStatusSpoiler(status.id)); }; useLayoutEffect(() => { maybeSetCollapsed(); maybeSetOnlyEmoji(); }); const parsedHtml = useMemo( (): string => translatable && status.translation ? status.translation.content! : (status.contentMapHtml && status.currentLanguage) ? (status.contentMapHtml[status.currentLanguage] || status.contentHtml) : status.contentHtml, [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]); const withSpoiler = status.spoiler_text.length > 0; const spoilerText = status.spoilerMapHtml && status.currentLanguage ? status.spoilerMapHtml[status.currentLanguage] || status.spoilerHtml : status.spoilerHtml; const direction = getTextDirection(status.search_index); const className = clsx('relative overflow-hidden text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', { 'cursor-pointer': onClick, 'max-h-[200px]': collapsed, 'leading-normal big-emoji': onlyEmoji, }); const expandable = !displaySpoilers; const expanded = !withSpoiler || status.expanded || false; const output = []; if (spoilerText) { output.push( {content && expandable && ( )} , ); } if (expandable && !expanded) return <>{output}; if (onClick) { if (content) { output.push( {content} , ); } const hasPoll = !!status.poll_id; if (collapsed) { output.push(); } if (status.poll_id) { output.push(); } return {output}; } else { if (content) { output.push( {content} , ); } if (collapsed) { output.push( {}} key='read-more' quote={quote} />); } if (status.poll_id) { output.push(); } return <>{output}; } }); export { StatusContent as default };