2
0

Fixes handled link formatting (#36410)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo
2025-10-09 16:31:13 +02:00
committed by GitHub
parent 258869278e
commit c858fc77ef
3 changed files with 60 additions and 34 deletions

View File

@@ -1,19 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react-vite'; import type { Meta, StoryObj } from '@storybook/react-vite';
import { HashtagMenuController } from '@/mastodon/features/ui/components/hashtag_menu_controller'; import { HashtagMenuController } from '@/mastodon/features/ui/components/hashtag_menu_controller';
import { accountFactoryState } from '@/testing/factories';
import { HoverCardController } from '../hover_card_controller'; import { HoverCardController } from '../hover_card_controller';
import type { HandledLinkProps } from './handled_link'; import type { HandledLinkProps } from './handled_link';
import { HandledLink } from './handled_link'; import { HandledLink } from './handled_link';
type HandledLinkStoryProps = Pick<HandledLinkProps, 'href' | 'text'> & { type HandledLinkStoryProps = Pick<
HandledLinkProps,
'href' | 'text' | 'prevText'
> & {
mentionAccount: 'local' | 'remote' | 'none'; mentionAccount: 'local' | 'remote' | 'none';
hashtagAccount: boolean;
}; };
const meta = { const meta = {
title: 'Components/Status/HandledLink', title: 'Components/Status/HandledLink',
render({ mentionAccount, ...args }) { render({ mentionAccount, hashtagAccount, ...args }) {
let mention: HandledLinkProps['mention'] | undefined; let mention: HandledLinkProps['mention'] | undefined;
if (mentionAccount === 'local') { if (mentionAccount === 'local') {
mention = { id: '1', acct: 'testuser' }; mention = { id: '1', acct: 'testuser' };
@@ -22,7 +27,13 @@ const meta = {
} }
return ( return (
<> <>
<HandledLink {...args} mention={mention} hashtagAccountId='1' /> <HandledLink
{...args}
mention={mention}
hashtagAccountId={hashtagAccount ? '1' : undefined}
>
<span>{args.text}</span>
</HandledLink>
<HashtagMenuController /> <HashtagMenuController />
<HoverCardController /> <HoverCardController />
</> </>
@@ -32,6 +43,7 @@ const meta = {
href: 'https://example.com/path/subpath?query=1#hash', href: 'https://example.com/path/subpath?query=1#hash',
text: 'https://example.com', text: 'https://example.com',
mentionAccount: 'none', mentionAccount: 'none',
hashtagAccount: false,
}, },
argTypes: { argTypes: {
mentionAccount: { mentionAccount: {
@@ -40,6 +52,13 @@ const meta = {
defaultValue: 'none', defaultValue: 'none',
}, },
}, },
parameters: {
state: {
accounts: {
'1': accountFactoryState({ id: '1', acct: 'hashtaguser' }),
},
},
},
} satisfies Meta<HandledLinkStoryProps>; } satisfies Meta<HandledLinkStoryProps>;
export default meta; export default meta;
@@ -48,9 +67,16 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {}; export const Default: Story = {};
export const Simple: Story = {
args: {
href: 'https://example.com/test',
},
};
export const Hashtag: Story = { export const Hashtag: Story = {
args: { args: {
text: '#example', text: '#example',
hashtagAccount: true,
}, },
}; };

View File

@@ -10,6 +10,7 @@ import type { OnElementHandler } from '@/mastodon/utils/html';
export interface HandledLinkProps { export interface HandledLinkProps {
href: string; href: string;
text: string; text: string;
prevText?: string;
hashtagAccountId?: string; hashtagAccountId?: string;
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>; mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
} }
@@ -17,13 +18,15 @@ export interface HandledLinkProps {
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
href, href,
text, text,
prevText,
hashtagAccountId, hashtagAccountId,
mention, mention,
className, className,
children,
...props ...props
}) => { }) => {
// Handle hashtags // Handle hashtags
if (text.startsWith('#')) { if (text.startsWith('#') || prevText?.endsWith('#')) {
const hashtag = text.slice(1).trim(); const hashtag = text.slice(1).trim();
return ( return (
<Link <Link
@@ -32,10 +35,10 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
rel='tag' rel='tag'
data-menu-hashtag={hashtagAccountId} data-menu-hashtag={hashtagAccountId}
> >
#<span>{hashtag}</span> {children}
</Link> </Link>
); );
} else if (text.startsWith('@') && mention) { } else if ((text.startsWith('@') || prevText?.endsWith('@')) && mention) {
// Handle mentions // Handle mentions
return ( return (
<Link <Link
@@ -44,41 +47,33 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
title={`@${mention.acct}`} title={`@${mention.acct}`}
data-hover-card-account={mention.id} data-hover-card-account={mention.id}
> >
@<span>{text.slice(1).trim()}</span> {children}
</Link> </Link>
); );
} }
// Non-absolute paths treated as internal links. // Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
if (href.startsWith('/')) { if (href.startsWith('/')) {
return ( return (
<Link className={classNames('unhandled-link', className)} to={href}> <Link className={classNames('unhandled-link', className)} to={href}>
{text} {children}
</Link> </Link>
); );
} }
try { return (
const url = new URL(href); <a
const [first, ...rest] = url.pathname.split('/').slice(1); // Start at 1 to skip the leading slash. {...props}
return ( href={href}
<a title={href}
{...props} className={classNames('unhandled-link', className)}
href={href} target='_blank'
title={href} rel='noreferrer noopener'
className={classNames('unhandled-link', className)} translate='no'
target='_blank' >
rel='noreferrer noopener' {children}
translate='no' </a>
> );
<span className='invisible'>{url.protocol + '//'}</span>
<span className='ellipsis'>{`${url.hostname}/${first ?? ''}`}</span>
<span className='invisible'>{'/' + rest.join('/')}</span>
</a>
);
} catch {
return text;
}
}; };
export const useElementHandledLink = ({ export const useElementHandledLink = ({
@@ -89,7 +84,7 @@ export const useElementHandledLink = ({
hrefToMention?: (href: string) => ApiMentionJSON | undefined; hrefToMention?: (href: string) => ApiMentionJSON | undefined;
} = {}) => { } = {}) => {
const onElement = useCallback<OnElementHandler>( const onElement = useCallback<OnElementHandler>(
(element, { key, ...props }) => { (element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) { if (element instanceof HTMLAnchorElement) {
const mention = hrefToMention?.(element.href); const mention = hrefToMention?.(element.href);
return ( return (
@@ -98,9 +93,12 @@ export const useElementHandledLink = ({
key={key as string} // React requires keys to not be part of spread props. key={key as string} // React requires keys to not be part of spread props.
href={element.href} href={element.href}
text={element.innerText} text={element.innerText}
prevText={element.previousSibling?.textContent ?? undefined}
hashtagAccountId={hashtagAccountId} hashtagAccountId={hashtagAccountId}
mention={mention} mention={mention}
/> >
{children}
</HandledLink>
); );
} }
return undefined; return undefined;

View File

@@ -204,7 +204,7 @@ class StatusContent extends PureComponent {
this.node = c; this.node = c;
}; };
handleElement = (element, { key, ...props }) => { handleElement = (element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) { if (element instanceof HTMLAnchorElement) {
const mention = this.props.status.get('mentions').find(item => element.href === item.get('url')); const mention = this.props.status.get('mentions').find(item => element.href === item.get('url'));
return ( return (
@@ -215,7 +215,9 @@ class StatusContent extends PureComponent {
hashtagAccountId={this.props.status.getIn(['account', 'id'])} hashtagAccountId={this.props.status.getIn(['account', 'id'])}
mention={mention?.toJSON()} mention={mention?.toJSON()}
key={key} key={key}
/> >
{children}
</HandledLink>
); );
} else if (element instanceof HTMLParagraphElement && element.classList.contains('quote-inline')) { } else if (element instanceof HTMLParagraphElement && element.classList.contains('quote-inline')) {
return null; return null;