pl-fe: handle mentions and hashtag links in bio
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
db3c895731
commit
752f10fac2
3 changed files with 148 additions and 59 deletions
|
@ -109,69 +109,71 @@ const StatusContent: React.FC<IStatusContent> = 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 (
|
||||
<HoverRefWrapper accountId={mention.id} inline>
|
||||
<Link
|
||||
to={`/@${mention.acct}`}
|
||||
className='text-primary-600 hover:underline dark:text-accent-blue'
|
||||
dir='ltr'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{mention.username}
|
||||
</Link>
|
||||
</HoverRefWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (classes?.includes('hashtag')) {
|
||||
const child = domToReact(domNode.children as DOMNode[]);
|
||||
const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined;
|
||||
if (hashtag) {
|
||||
return <HashtagLink hashtag={hashtag} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
{...domNode.attribs}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel='nofollow noopener'
|
||||
target='_blank'
|
||||
title={domNode.attribs.href}
|
||||
>
|
||||
{domToReact(domNode.children as DOMNode[], options)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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 (
|
||||
<HoverRefWrapper accountId={mention.id} inline>
|
||||
<Link
|
||||
to={`/@${mention.acct}`}
|
||||
className='text-primary-600 hover:underline dark:text-accent-blue'
|
||||
dir='ltr'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{mention.username}
|
||||
</Link>
|
||||
</HoverRefWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (classes?.includes('hashtag')) {
|
||||
const child = domToReact(domNode.children as DOMNode[]);
|
||||
const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined;
|
||||
if (hashtag) {
|
||||
return <HashtagLink hashtag={hashtag} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
{...domNode.attribs}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel='nofollow noopener'
|
||||
target='_blank'
|
||||
title={domNode.attribs.href}
|
||||
>
|
||||
{domToReact(domNode.children as DOMNode[], options)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
|
|
36
packages/pl-fe/src/components/status-mention.tsx
Normal file
36
packages/pl-fe/src/components/status-mention.tsx
Normal file
|
@ -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<IStatusMention> = ({ accountId, fallback }) => {
|
||||
const { account } = useAccount(accountId);
|
||||
|
||||
if (!account) return (
|
||||
<HoverRefWrapper accountId={accountId} inline>
|
||||
{fallback}
|
||||
</HoverRefWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<HoverRefWrapper accountId={accountId} inline>
|
||||
<Link
|
||||
to={`/@${account.acct}`}
|
||||
className='text-primary-600 hover:underline dark:text-accent-blue'
|
||||
dir='ltr'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{account.acct}
|
||||
</Link>
|
||||
</HoverRefWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusMention as default };
|
|
@ -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<IProfileInfoPanel> = ({ 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
|
||||
<a
|
||||
{...domNode.attribs}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel='nofollow noopener'
|
||||
target='_blank'
|
||||
title={domNode.attribs.href}
|
||||
>
|
||||
{domToReact(domNode.children as DOMNode[], options)}
|
||||
</a>
|
||||
);
|
||||
|
||||
if (classes?.includes('mention') && id) {
|
||||
return (
|
||||
<StatusMention accountId={id} fallback={fallback} />
|
||||
);
|
||||
}
|
||||
|
||||
if (classes?.includes('hashtag')) {
|
||||
const child = domToReact(domNode.children as DOMNode[]);
|
||||
const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined;
|
||||
if (hashtag) {
|
||||
return <HashtagLink hashtag={hashtag} />;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return !!account.note.length && parse(account.note_emojified, options);
|
||||
}, [account?.note_emojified]);
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
||||
|
@ -155,8 +206,8 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
|
||||
<ProfileStats account={account} />
|
||||
|
||||
{account.note.length > 0 && (
|
||||
<Markup size='sm' dangerouslySetInnerHTML={{ __html: account.note_emojified }} />
|
||||
{note && (
|
||||
<Markup size='sm'>{note}</Markup>
|
||||
)}
|
||||
|
||||
<div className='flex flex-col items-start gap-2 md:flex-row md:flex-wrap md:items-center'>
|
||||
|
|
Loading…
Reference in a new issue