Emoji: Link Replacement (#36341)
This commit is contained in:
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;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user