Emoji: Link Replacement (#36341)

This commit is contained in:
Echo authored and GitHub committed 2025-10-06 09:31:10 +00:00
1 parent 4dc21d7afd
commit ffac4cb05f
7 files changed
+346 -116

No files matched your search

@@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { HashtagMenuController } from '@/mastodon/features/ui/components/hashtag_menu_controller';
import { accountFactoryState } from '@/testing/factories';
import { HoverCardController } from '../hover_card_controller';
import type { HandledLinkProps } from './handled_link';
import { HandledLink } from './handled_link';
const meta = {
title: 'Components/Status/HandledLink',
render(args) {
return (
<>
<HandledLink {...args} mentionAccountId='1' hashtagAccountId='1' />
<HashtagMenuController />
<HoverCardController />
</>
);
},
args: {
href: 'https://example.com/path/subpath?query=1#hash',
text: 'https://example.com',
},
parameters: {
state: {
accounts: {
'1': accountFactoryState(),
},
},
},
} satisfies Meta<Pick<HandledLinkProps, 'href' | 'text'>>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Hashtag: Story = {
args: {
text: '#example',
},
};
export const Mention: Story = {
args: {
text: '@user',
},
};
export const InternalLink: Story = {
args: {
href: '/about',
text: 'About',
},
};
export const InvalidURL: Story = {
args: {
href: 'ht!tp://invalid-url',
text: 'ht!tp://invalid-url -- invalid!',
},
};
@@ -0,0 +1,79 @@
import type { ComponentProps, FC } from 'react';
import { Link } from 'react-router-dom';
export interface HandledLinkProps {
href: string;
text: string;
hashtagAccountId?: string;
mentionAccountId?: string;
}
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
href,
text,
hashtagAccountId,
mentionAccountId,
...props
}) => {
// Handle hashtags
if (text.startsWith('#')) {
const hashtag = text.slice(1).trim();
return (
<Link
{...props}
className='mention hashtag'
to={`/tags/${hashtag}`}
rel='tag'
data-menu-hashtag={hashtagAccountId}
>
#<span>{hashtag}</span>
</Link>
);
} else if (text.startsWith('@')) {
// Handle mentions
const mention = text.slice(1).trim();
return (
<Link
{...props}
className='mention'
to={`/@${mention}`}
title={`@${mention}`}
data-hover-card-account={mentionAccountId}
>
@<span>{mention}</span>
</Link>
);
}
// Non-absolute paths treated as internal links.
if (href.startsWith('/')) {
return (
<Link {...props} className='unhandled-link' to={href}>
{text}
</Link>
);
}
try {
const url = new URL(href);
const [first, ...rest] = url.pathname.split('/').slice(1); // Start at 1 to skip the leading slash.
return (
<a
{...props}
href={href}
title={href}
className='unhandled-link'
target='_blank'
rel='noreferrer noopener'
translate='no'
>
<span className='invisible'>{url.protocol + '//'}</span>
<span className='ellipsis'>{`${url.hostname}/${first ?? ''}`}</span>
<span className='invisible'>{'/' + rest.join('/')}</span>
</a>
);
} catch {
return text;
}
};