diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx index 301ffcbb2..e0127f209 100644 --- a/app/javascript/mastodon/components/account_bio.tsx +++ b/app/javascript/mastodon/components/account_bio.tsx @@ -1,12 +1,30 @@ +import { useCallback } from 'react'; + import { useLinks } from 'mastodon/hooks/useLinks'; -export const AccountBio: React.FC<{ +interface AccountBioProps { note: string; className: string; -}> = ({ note, className }) => { - const handleClick = useLinks(); + dropdownAccountId?: string; +} - if (note.length === 0 || note === '

') { +export const AccountBio: React.FC = ({ + note, + className, + dropdownAccountId, +}) => { + const handleClick = useLinks(!!dropdownAccountId); + const handleNodeChange = useCallback( + (node: HTMLDivElement | null) => { + if (!dropdownAccountId || !node || node.childNodes.length === 0) { + return; + } + addDropdownToHashtags(node, dropdownAccountId); + }, + [dropdownAccountId], + ); + + if (note.length === 0) { return null; } @@ -15,6 +33,28 @@ export const AccountBio: React.FC<{ className={`${className} translate`} dangerouslySetInnerHTML={{ __html: note }} onClickCapture={handleClick} + ref={handleNodeChange} /> ); }; + +function addDropdownToHashtags(node: HTMLElement | null, accountId: string) { + if (!node) { + return; + } + for (const childNode of node.childNodes) { + if (!(childNode instanceof HTMLElement)) { + continue; + } + if ( + childNode instanceof HTMLAnchorElement && + (childNode.classList.contains('hashtag') || + childNode.innerText.startsWith('#')) && + !childNode.dataset.menuHashtag + ) { + childNode.dataset.menuHashtag = accountId; + } else if (childNode.childNodes.length > 0) { + addDropdownToHashtags(childNode, accountId); + } + } +} diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 30433151c..b9f83beba 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -6,6 +6,7 @@ import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { NavLink } from 'react-router-dom'; +import { AccountBio } from '@/mastodon/components/account_bio'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; @@ -773,7 +774,6 @@ export const AccountHeader: React.FC<{ ); } - const content = { __html: account.note_emojified }; const displayNameHtml = { __html: account.display_name_html }; const fields = account.fields; const isLocal = !account.acct.includes('@'); @@ -897,12 +897,11 @@ export const AccountHeader: React.FC<{ )} - {account.note.length > 0 && account.note !== '

' && ( -
- )} +
diff --git a/app/javascript/mastodon/hooks/useLinks.ts b/app/javascript/mastodon/hooks/useLinks.ts index c99f3f419..160fe1850 100644 --- a/app/javascript/mastodon/hooks/useLinks.ts +++ b/app/javascript/mastodon/hooks/useLinks.ts @@ -8,13 +8,14 @@ import { openURL } from 'mastodon/actions/search'; import { useAppDispatch } from 'mastodon/store'; const isMentionClick = (element: HTMLAnchorElement) => - element.classList.contains('mention'); + element.classList.contains('mention') && + !element.classList.contains('hashtag'); const isHashtagClick = (element: HTMLAnchorElement) => element.textContent?.[0] === '#' || element.previousSibling?.textContent?.endsWith('#'); -export const useLinks = () => { +export const useLinks = (skipHashtags?: boolean) => { const history = useHistory(); const dispatch = useAppDispatch(); @@ -61,12 +62,12 @@ export const useLinks = () => { if (isMentionClick(target)) { e.preventDefault(); void handleMentionClick(target); - } else if (isHashtagClick(target)) { + } else if (isHashtagClick(target) && !skipHashtags) { e.preventDefault(); handleHashtagClick(target); } }, - [handleMentionClick, handleHashtagClick], + [skipHashtags, handleMentionClick, handleHashtagClick], ); return handleClick; diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index 2666059b4..75a5c09b9 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -126,6 +126,9 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { ? accountJSON.username : accountJSON.display_name; + const accountNote = + accountJSON.note && accountJSON.note !== '

' ? accountJSON.note : ''; + return AccountFactory({ ...accountJSON, moved: moved?.id, @@ -142,8 +145,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { escapeTextContentForBrowser(displayName), emojiMap, ), - note_emojified: emojify(accountJSON.note, emojiMap), - note_plain: unescapeHTML(accountJSON.note), + note_emojified: emojify(accountNote, emojiMap), + note_plain: unescapeHTML(accountNote), url: accountJSON.url.startsWith('http://') || accountJSON.url.startsWith('https://')