Merge remote-tracking branch 'parent/main' into upstream-20240628

This commit is contained in:
KMY 2024-06-28 08:57:12 +09:00
commit 6955843321
178 changed files with 1924 additions and 1178 deletions

View file

@ -0,0 +1,20 @@
import { useLinks } from 'mastodon/../hooks/useLinks';
export const AccountBio: React.FC<{
note: string;
className: string;
}> = ({ note, className }) => {
const handleClick = useLinks();
if (note.length === 0 || note === '<p></p>') {
return null;
}
return (
<div
className={`${className} translate`}
dangerouslySetInnerHTML={{ __html: note }}
onClickCapture={handleClick}
/>
);
};

View file

@ -0,0 +1,42 @@
import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { useLinks } from 'mastodon/../hooks/useLinks';
import { Icon } from 'mastodon/components/icon';
import type { Account } from 'mastodon/models/account';
export const AccountFields: React.FC<{
fields: Account['fields'];
limit: number;
}> = ({ fields, limit = -1 }) => {
const handleClick = useLinks();
if (fields.size === 0) {
return null;
}
return (
<div className='account-fields' onClickCapture={handleClick}>
{fields.take(limit).map((pair, i) => (
<dl
key={i}
className={classNames({ verified: pair.get('verified_at') })}
>
<dt
dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }}
className='translate'
/>
<dd className='translate' title={pair.get('value_plain') ?? ''}>
{pair.get('verified_at') && (
<Icon id='check' icon={CheckIcon} className='verified__mark' />
)}
<span
dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }}
/>
</dd>
</dl>
))}
</div>
);
};

View file

@ -1,233 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent, useCallback } from 'react';
import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import { Icon } from 'mastodon/components/icon';
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { useAppHistory } from './router';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
});
const BackButton = ({ onlyIcon }) => {
const history = useAppHistory();
const intl = useIntl();
const handleBackClick = useCallback(() => {
if (history.location?.state?.fromMastodon) {
history.goBack();
} else {
history.push('/');
}
}, [history]);
return (
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
</button>
);
};
BackButton.propTypes = {
onlyIcon: PropTypes.bool,
};
class ColumnHeader extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
intl: PropTypes.object.isRequired,
title: PropTypes.node,
icon: PropTypes.string,
iconComponent: PropTypes.func,
active: PropTypes.bool,
multiColumn: PropTypes.bool,
extraButton: PropTypes.node,
showBackButton: PropTypes.bool,
children: PropTypes.node,
pinned: PropTypes.bool,
placeholder: PropTypes.bool,
onPin: PropTypes.func,
onMove: PropTypes.func,
onClick: PropTypes.func,
appendContent: PropTypes.node,
collapseIssues: PropTypes.bool,
...WithRouterPropTypes,
};
state = {
collapsed: true,
animating: false,
};
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true });
};
handleTitleClick = () => {
this.props.onClick?.();
};
handleMoveLeft = () => {
this.props.onMove(-1);
};
handleMoveRight = () => {
this.props.onMove(1);
};
handleTransitionEnd = () => {
this.setState({ animating: false });
};
handlePin = () => {
if (!this.props.pinned) {
this.props.history.replace('/');
}
this.props.onPin();
};
render () {
const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
'active': active,
});
const buttonClassName = classNames('column-header', {
'active': active,
});
const collapsibleClassName = classNames('column-header__collapsible', {
'collapsed': collapsed,
'animating': animating,
});
const collapsibleButtonClassName = classNames('column-header__button', {
'active': !collapsed,
});
let extraContent, pinButton, moveButtons, backButton, collapseButton;
if (children) {
extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'>
{children}
</div>
);
}
if (multiColumn && pinned) {
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
moveButtons = (
<div className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
</div>
);
} else if (multiColumn && this.props.onPin) {
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
}
if (history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
backButton = <BackButton onlyIcon={!!title} />;
}
const collapsedContent = [
extraContent,
];
if (multiColumn) {
collapsedContent.push(
<div key='buttons' className='column-header__advanced-buttons'>
{pinButton}
{moveButtons}
</div>
);
}
if (this.props.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
collapseButton = (
<button
className={collapsibleButtonClassName}
title={formatMessage(collapsed ? messages.show : messages.hide)}
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
onClick={this.handleToggleClick}
>
<i className='icon-with-badge'>
<Icon id='sliders' icon={SettingsIcon} />
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
</i>
</button>
);
}
const hasTitle = (icon || iconComponent) && title;
const component = (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
{hasTitle && (
<>
{backButton}
<button onClick={this.handleTitleClick} className='column-header__title'>
{!backButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
{title}
</button>
</>
)}
{!hasTitle && backButton}
<div className='column-header__buttons'>
{extraButton}
{collapseButton}
</div>
</h1>
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent}
</div>
</div>
{appendContent}
</div>
);
if (placeholder) {
return component;
} else {
return (<ButtonInTabsBar>
{component}
</ButtonInTabsBar>);
}
}
}
export default injectIntl(withIdentity(withRouter(ColumnHeader)));

View file

@ -0,0 +1,301 @@
import { useCallback, useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
import { useIdentity } from 'mastodon/identity_context';
import { useAppHistory } from './router';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: {
id: 'column_header.moveLeft_settings',
defaultMessage: 'Move column to the left',
},
moveRight: {
id: 'column_header.moveRight_settings',
defaultMessage: 'Move column to the right',
},
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
});
const BackButton: React.FC<{
onlyIcon: boolean;
}> = ({ onlyIcon }) => {
const history = useAppHistory();
const intl = useIntl();
const handleBackClick = useCallback(() => {
if (history.location.state?.fromMastodon) {
history.goBack();
} else {
history.push('/');
}
}, [history]);
return (
<button
onClick={handleBackClick}
className={classNames('column-header__back-button', {
compact: onlyIcon,
})}
aria-label={intl.formatMessage(messages.back)}
>
<Icon
id='chevron-left'
icon={ArrowBackIcon}
className='column-back-button__icon'
/>
{!onlyIcon && (
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
)}
</button>
);
};
export interface Props {
title?: string;
icon?: string;
iconComponent?: IconProp;
active?: boolean;
children?: React.ReactNode;
pinned?: boolean;
multiColumn?: boolean;
extraButton?: React.ReactNode;
showBackButton?: boolean;
placeholder?: boolean;
appendContent?: React.ReactNode;
collapseIssues?: boolean;
onClick?: () => void;
onMove?: (arg0: number) => void;
onPin?: () => void;
}
export const ColumnHeader: React.FC<Props> = ({
title,
icon,
iconComponent,
active,
children,
pinned,
multiColumn,
extraButton,
showBackButton,
placeholder,
appendContent,
collapseIssues,
onClick,
onMove,
onPin,
}) => {
const intl = useIntl();
const { signedIn } = useIdentity();
const history = useAppHistory();
const [collapsed, setCollapsed] = useState(true);
const [animating, setAnimating] = useState(false);
const handleToggleClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
setCollapsed((value) => !value);
setAnimating(true);
},
[setCollapsed, setAnimating],
);
const handleTitleClick = useCallback(() => {
onClick?.();
}, [onClick]);
const handleMoveLeft = useCallback(() => {
onMove?.(-1);
}, [onMove]);
const handleMoveRight = useCallback(() => {
onMove?.(1);
}, [onMove]);
const handleTransitionEnd = useCallback(() => {
setAnimating(false);
}, [setAnimating]);
const handlePin = useCallback(() => {
if (!pinned) {
history.replace('/');
}
onPin?.();
}, [history, pinned, onPin]);
const wrapperClassName = classNames('column-header__wrapper', {
active,
});
const buttonClassName = classNames('column-header', {
active,
});
const collapsibleClassName = classNames('column-header__collapsible', {
collapsed,
animating,
});
const collapsibleButtonClassName = classNames('column-header__button', {
active: !collapsed,
});
let extraContent, pinButton, moveButtons, backButton, collapseButton;
if (children) {
extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'>
{children}
</div>
);
}
if (multiColumn && pinned) {
pinButton = (
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
>
<Icon id='times' icon={CloseIcon} />{' '}
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
</button>
);
moveButtons = (
<div className='column-header__setting-arrows'>
<button
title={intl.formatMessage(messages.moveLeft)}
aria-label={intl.formatMessage(messages.moveLeft)}
className='icon-button column-header__setting-btn'
onClick={handleMoveLeft}
>
<Icon id='chevron-left' icon={ChevronLeftIcon} />
</button>
<button
title={intl.formatMessage(messages.moveRight)}
aria-label={intl.formatMessage(messages.moveRight)}
className='icon-button column-header__setting-btn'
onClick={handleMoveRight}
>
<Icon id='chevron-right' icon={ChevronRightIcon} />
</button>
</div>
);
} else if (multiColumn && onPin) {
pinButton = (
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
>
<Icon id='plus' icon={AddIcon} />{' '}
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
</button>
);
}
if (
!pinned &&
((multiColumn && history.location.state?.fromMastodon) || showBackButton)
) {
backButton = <BackButton onlyIcon={!!title} />;
}
const collapsedContent = [extraContent];
if (multiColumn) {
collapsedContent.push(
<div key='buttons' className='column-header__advanced-buttons'>
{pinButton}
{moveButtons}
</div>,
);
}
if (signedIn && (children || (multiColumn && onPin))) {
collapseButton = (
<button
className={collapsibleButtonClassName}
title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
aria-label={intl.formatMessage(
collapsed ? messages.show : messages.hide,
)}
onClick={handleToggleClick}
>
<i className='icon-with-badge'>
<Icon id='sliders' icon={SettingsIcon} />
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
</i>
</button>
);
}
const hasIcon = icon && iconComponent;
const hasTitle = hasIcon && title;
const component = (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
{hasTitle && (
<>
{backButton}
<button onClick={handleTitleClick} className='column-header__title'>
{!backButton && (
<Icon
id={icon}
icon={iconComponent}
className='column-header__icon'
/>
)}
{title}
</button>
</>
)}
{!hasTitle && backButton}
<div className='column-header__buttons'>
{extraButton}
{collapseButton}
</div>
</h1>
<div
className={collapsibleClassName}
tabIndex={collapsed ? -1 : undefined}
onTransitionEnd={handleTransitionEnd}
>
<div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent}
</div>
</div>
{appendContent}
</div>
);
if (placeholder) {
return component;
} else {
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
}
};
// eslint-disable-next-line import/no-default-export
export default ColumnHeader;

View file

@ -0,0 +1,125 @@
import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { useIdentity } from '@/mastodon/identity_context';
import {
fetchRelationships,
followAccount,
unfollowAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { me, isShowItem } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
cancel_follow_request: {
id: 'account.cancel_follow_request',
defaultMessage: 'Withdraw follow request',
},
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
export const FollowButton: React.FC<{
accountId: string;
}> = ({ accountId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const relationship = useAppSelector((state) =>
state.relationships.get(accountId),
);
const following = relationship?.following || relationship?.requested;
useEffect(() => {
if (accountId && signedIn) {
dispatch(fetchRelationships([accountId]));
}
}, [dispatch, accountId, signedIn]);
const handleClick = useCallback(() => {
if (!signedIn) {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'follow',
accountId: accountId,
url: account?.url,
},
}),
);
}
if (!relationship) return;
if (accountId === me) {
return;
} else if (relationship.following || relationship.requested) {
dispatch(unfollowAccount(accountId));
} else {
dispatch(followAccount(accountId));
}
}, [dispatch, accountId, relationship, account, signedIn]);
let label;
if (!signedIn) {
label = intl.formatMessage(messages.follow);
} else if (accountId === me) {
label = intl.formatMessage(messages.edit_profile);
} else if (!relationship) {
label = <LoadingIndicator />;
} else if (relationship.requested) {
label = intl.formatMessage(messages.cancel_follow_request);
} else if (
relationship.following &&
isShowItem('relationships') &&
relationship.followed_by
) {
label = intl.formatMessage(messages.mutual);
} else if (
!relationship.following &&
isShowItem('relationships') &&
relationship.followed_by
) {
label = intl.formatMessage(messages.followBack);
} else if (relationship.following) {
label = intl.formatMessage(messages.unfollow);
} else {
label = intl.formatMessage(messages.follow);
}
if (accountId === me) {
return (
<a
href='/settings/profile'
target='_blank'
rel='noreferrer noopener'
className='button button-secondary'
>
{label}
</a>
);
}
return (
<Button
onClick={handleClick}
disabled={relationship?.blocked_by || relationship?.blocking}
secondary={following}
className={following ? 'button--destructive' : undefined}
>
{label}
</Button>
);
};

View file

@ -0,0 +1,75 @@
import { useEffect, forwardRef } from 'react';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { fetchAccount } from 'mastodon/actions/accounts';
import { AccountBio } from 'mastodon/components/account_bio';
import { AccountFields } from 'mastodon/components/account_fields';
import { Avatar } from 'mastodon/components/avatar';
import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ShortNumber } from 'mastodon/components/short_number';
import { domain } from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
export const HoverCardAccount = forwardRef<
HTMLDivElement,
{ accountId: string }
>(({ accountId }, ref) => {
const dispatch = useAppDispatch();
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
useEffect(() => {
if (accountId && !account) {
dispatch(fetchAccount(accountId));
}
}, [dispatch, accountId, account]);
return (
<div
ref={ref}
id='hover-card'
role='tooltip'
className={classNames('hover-card dropdown-animation', {
'hover-card--loading': !account,
})}
>
{account ? (
<>
<Link to={`/@${account.acct}`} className='hover-card__name'>
<Avatar account={account} size={46} />
<DisplayName account={account} localDomain={domain} />
</Link>
<div className='hover-card__text-row'>
<AccountBio
note={account.note_emojified}
className='hover-card__bio'
/>
<AccountFields fields={account.fields} limit={2} />
</div>
<div className='hover-card__number'>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
isHide={account.other_settings.hide_followers_count}
/>
</div>
<FollowButton accountId={accountId} />
</>
) : (
<LoadingIndicator />
)}
</div>
);
});
HoverCardAccount.displayName = 'HoverCardAccount';

View file

@ -0,0 +1,117 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
const offset = [-12, 4] as OffsetValue;
const enterDelay = 650;
const leaveDelay = 250;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
const isHoverCardAnchor = (element: HTMLElement) =>
element.matches('[data-hover-card-account]');
export const HoverCardController: React.FC = () => {
const [open, setOpen] = useState(false);
const [accountId, setAccountId] = useState<string | undefined>();
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
const cardRef = useRef<HTMLDivElement>(null);
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
const location = useLocation();
const handleAnchorMouseEnter = useCallback(
(e: MouseEvent) => {
const { target } = e;
if (target instanceof HTMLElement && isHoverCardAnchor(target)) {
cancelLeaveTimeout();
setEnterTimeout(() => {
target.setAttribute('aria-describedby', 'hover-card');
setAnchor(target);
setOpen(true);
setAccountId(
target.getAttribute('data-hover-card-account') ?? undefined,
);
}, enterDelay);
}
if (target === cardRef.current?.parentNode) {
cancelLeaveTimeout();
}
},
[cancelLeaveTimeout, setEnterTimeout, setOpen, setAccountId, setAnchor],
);
const handleAnchorMouseLeave = useCallback(
(e: MouseEvent) => {
if (e.target === anchor || e.target === cardRef.current?.parentNode) {
cancelEnterTimeout();
setLeaveTimeout(() => {
anchor?.removeAttribute('aria-describedby');
setOpen(false);
setAnchor(null);
}, leaveDelay);
}
},
[cancelEnterTimeout, setLeaveTimeout, setOpen, setAnchor, anchor],
);
const handleClose = useCallback(() => {
cancelEnterTimeout();
cancelLeaveTimeout();
setOpen(false);
setAnchor(null);
}, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]);
useEffect(() => {
handleClose();
}, [handleClose, location]);
useEffect(() => {
document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
passive: true,
capture: true,
});
document.body.addEventListener('mouseleave', handleAnchorMouseLeave, {
passive: true,
capture: true,
});
return () => {
document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
};
}, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
if (!accountId) return null;
return (
<Overlay
rootClose
onHide={handleClose}
show={open}
target={anchor}
placement='bottom-start'
flip
offset={offset}
popperConfig={popperConfig}
>
{({ props }) => (
<div {...props} className='hover-card-controller'>
<HoverCardAccount accountId={accountId} ref={cardRef} />
</div>
)}
</Overlay>
);
};

View file

@ -421,7 +421,7 @@ class Status extends ImmutablePureComponent {
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' icon={RepeatIcon} className='status__prepend-icon' /></div>
<div className='status__prepend-icon-wrapper'><VisibilityIcon visibility={visibilityName} className='status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
@ -442,7 +442,7 @@ class Status extends ImmutablePureComponent {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='reply' icon={ReplyIcon} className='status__prepend-icon' /></div>
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
}
@ -641,7 +641,7 @@ class Status extends ImmutablePureComponent {
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>

View file

@ -116,8 +116,9 @@ class StatusContent extends PureComponent {
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', `@${mention.get('acct')}`);
link.removeAttribute('title');
link.setAttribute('href', `/@${mention.get('acct')}`);
link.setAttribute('data-hover-card-account', mention.get('id'));
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);