Fixes handled link formatting (#36410)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,23 +47,20 @@ 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 {
|
|
||||||
const url = new URL(href);
|
|
||||||
const [first, ...rest] = url.pathname.split('/').slice(1); // Start at 1 to skip the leading slash.
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
{...props}
|
{...props}
|
||||||
@@ -71,14 +71,9 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
rel='noreferrer noopener'
|
rel='noreferrer noopener'
|
||||||
translate='no'
|
translate='no'
|
||||||
>
|
>
|
||||||
<span className='invisible'>{url.protocol + '//'}</span>
|
{children}
|
||||||
<span className='ellipsis'>{`${url.hostname}/${first ?? ''}`}</span>
|
|
||||||
<span className='invisible'>{'/' + rest.join('/')}</span>
|
|
||||||
</a>
|
</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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user