diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index e578deca5..9cd4890c5 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -109,69 +109,71 @@ const StatusContent: React.FC = React.memo(({ [status.contentHtml, status.translation, status.currentLanguage], ); - if (status.content.length === 0) { - return null; - } + 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 child = domToReact(domNode.children as DOMNode[]); + const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined; + 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]); const withSpoiler = status.spoiler_text.length > 0; - 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 child = domToReact(domNode.children as DOMNode[]); - const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined; - 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)} - - ); - } - }, - }; - const spoilerText = status.spoilerMapHtml && status.currentLanguage ? status.spoilerMapHtml[status.currentLanguage] || status.spoilerHtml : status.spoilerHtml; - const content = parse(parsedHtml, options); - 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, diff --git a/packages/pl-fe/src/components/status-mention.tsx b/packages/pl-fe/src/components/status-mention.tsx new file mode 100644 index 000000000..438fa5a63 --- /dev/null +++ b/packages/pl-fe/src/components/status-mention.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { useAccount } from 'pl-fe/api/hooks'; + +import HoverRefWrapper from './hover-ref-wrapper'; + +interface IStatusMention { + accountId: string; + fallback?: JSX.Element; +} + +const StatusMention: React.FC = ({ accountId, fallback }) => { + const { account } = useAccount(accountId); + + if (!account) return ( + + {fallback} + + ); + + return ( + + e.stopPropagation()} + > + @{account.acct} + + + ); +}; + +export { StatusMention as default }; diff --git a/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx b/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx index 55654349d..0d8e33648 100644 --- a/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx @@ -1,10 +1,13 @@ -import React from 'react'; +import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser'; +import React, { useMemo } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import Badge from 'pl-fe/components/badge'; +import HashtagLink from 'pl-fe/components/hashtag-link'; import Markup from 'pl-fe/components/markup'; import { dateFormatOptions } from 'pl-fe/components/relative-timestamp'; import Scrobble from 'pl-fe/components/scrobble'; +import StatusMention from 'pl-fe/components/status-mention'; import { Icon, HStack, Stack, Text } from 'pl-fe/components/ui'; import { useAppSelector, usePlFeConfig } from 'pl-fe/hooks'; import { capitalize } from 'pl-fe/utils/strings'; @@ -101,6 +104,54 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => ); }; + const note = useMemo(() => { + if (!account) return false; + + 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 id = domNode.attribs['data-user']; + + 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') && id) { + return ( + + ); + } + + if (classes?.includes('hashtag')) { + const child = domToReact(domNode.children as DOMNode[]); + const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined; + if (hashtag) { + return ; + } + } + + return fallback; + } + }, + }; + + return !!account.note.length && parse(account.note_emojified, options); + }, [account?.note_emojified]); + if (!account) { return (
@@ -155,8 +206,8 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => - {account.note.length > 0 && ( - + {note && ( + {note} )}