mirror of
https://github.com/mastodon/mastodon.git
synced 2026-03-21 18:05:23 -05:00
Refactors header from Status component (#37732)
This commit is contained in:
parent
ee631bf826
commit
7f53a77fa3
|
|
@ -3,14 +3,12 @@ import PropTypes from 'prop-types';
|
|||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||
|
|
@ -26,16 +24,12 @@ import { MediaGallery, Video, Audio } from '../features/ui/util/async-components
|
|||
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
|
||||
import { displayMedia } from '../initial_state';
|
||||
|
||||
import { Avatar } from './avatar';
|
||||
import { AvatarOverlay } from './avatar_overlay';
|
||||
import { StatusHeader } from './status/header'
|
||||
import { LinkedDisplayName } from './display_name';
|
||||
import { getHashtagBarForStatus } from './hashtag_bar';
|
||||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusContent from './status_content';
|
||||
import { StatusThreadLabel } from './status_thread_label';
|
||||
import { VisibilityIcon } from './visibility_icon';
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
|
|
@ -112,7 +106,6 @@ class Status extends ImmutablePureComponent {
|
|||
onToggleCollapsed: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
onQuoteCancel: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
|
|
@ -129,6 +122,7 @@ class Status extends ImmutablePureComponent {
|
|||
avatarSize: PropTypes.number,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
unfocusable: PropTypes.bool,
|
||||
headerRenderFn: PropTypes.func,
|
||||
pictureInPicture: ImmutablePropTypes.contains({
|
||||
inUse: PropTypes.bool,
|
||||
available: PropTypes.bool,
|
||||
|
|
@ -146,7 +140,6 @@ class Status extends ImmutablePureComponent {
|
|||
'hidden',
|
||||
'unread',
|
||||
'pictureInPicture',
|
||||
'onQuoteCancel',
|
||||
];
|
||||
|
||||
state = {
|
||||
|
|
@ -364,10 +357,6 @@ class Status extends ImmutablePureComponent {
|
|||
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
|
||||
};
|
||||
|
||||
handleQuoteCancel = () => {
|
||||
this.props.onQuoteCancel?.();
|
||||
}
|
||||
|
||||
_properStatus () {
|
||||
const { status } = this.props;
|
||||
|
||||
|
|
@ -383,7 +372,24 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { intl, hidden, featured, unfocusable, unread, showThread, showActions = true, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
|
||||
const {
|
||||
intl,
|
||||
hidden,
|
||||
featured,
|
||||
unfocusable,
|
||||
unread,
|
||||
showThread,
|
||||
showActions = true,
|
||||
isQuotedPost = false,
|
||||
scrollKey,
|
||||
pictureInPicture,
|
||||
previousId,
|
||||
nextInReplyToId,
|
||||
rootId,
|
||||
skipPrepend,
|
||||
avatarSize = 46,
|
||||
children,
|
||||
} = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
|
|
@ -405,7 +411,7 @@ class Status extends ImmutablePureComponent {
|
|||
onTranslate: this.handleTranslate,
|
||||
};
|
||||
|
||||
let media, statusAvatar, prepend, rebloggedByText;
|
||||
let media, prepend, rebloggedByText;
|
||||
|
||||
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
||||
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
|
||||
|
|
@ -547,13 +553,19 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
if (account === undefined || account === null) {
|
||||
statusAvatar = <Avatar account={status.get('account')} size={avatarSize} />;
|
||||
} else {
|
||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
|
||||
const header = this.props.headerRenderFn
|
||||
? this.props.headerRenderFn({ status, account, avatarSize, messages, onHeaderClick: this.handleHeaderClick })
|
||||
: (
|
||||
<StatusHeader
|
||||
status={status}
|
||||
account={account}
|
||||
avatarSize={avatarSize}
|
||||
onHeaderClick={this.handleHeaderClick}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Hotkeys handlers={handlers} focusable={!unfocusable}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader({intl, status, rebloggedByText, isQuote: isQuotedPost})} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||
|
|
@ -575,28 +587,7 @@ class Status extends ImmutablePureComponent {
|
|||
>
|
||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||
|
||||
<div onClick={this.handleHeaderClick} onAuxClick={this.handleHeaderClick} className='status__info'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time'>
|
||||
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
|
||||
<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>}
|
||||
</Link>
|
||||
|
||||
<LinkedDisplayName displayProps={{account: status.get('account')}} className='status__display-name'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
</LinkedDisplayName>
|
||||
|
||||
{isQuotedPost && !!this.props.onQuoteCancel && (
|
||||
<IconButton
|
||||
onClick={this.handleQuoteCancel}
|
||||
className='status__quote-cancel'
|
||||
title={intl.formatMessage(messages.quote_cancel)}
|
||||
icon="cancel-fill"
|
||||
iconComponent={CancelFillIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{header}
|
||||
|
||||
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
||||
|
||||
|
|
|
|||
128
app/javascript/mastodon/components/status/header.tsx
Normal file
128
app/javascript/mastodon/components/status/header.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import type { FC, HTMLAttributes, MouseEventHandler, ReactNode } from 'react';
|
||||
|
||||
import { defineMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { isStatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type { Status } from '@/mastodon/models/status';
|
||||
|
||||
import { Avatar } from '../avatar';
|
||||
import { AvatarOverlay } from '../avatar_overlay';
|
||||
import type { DisplayNameProps } from '../display_name';
|
||||
import { LinkedDisplayName } from '../display_name';
|
||||
import { RelativeTimestamp } from '../relative_timestamp';
|
||||
import { VisibilityIcon } from '../visibility_icon';
|
||||
|
||||
export interface StatusHeaderProps {
|
||||
status: Status;
|
||||
account?: Account;
|
||||
avatarSize?: number;
|
||||
children?: ReactNode;
|
||||
wrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||
displayNameProps?: DisplayNameProps;
|
||||
onHeaderClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode;
|
||||
|
||||
export const StatusHeader: FC<StatusHeaderProps> = ({
|
||||
status,
|
||||
account,
|
||||
children,
|
||||
avatarSize = 48,
|
||||
wrapperProps,
|
||||
onHeaderClick,
|
||||
}) => {
|
||||
const statusAccount = status.get('account') as Account | undefined;
|
||||
const editedAt = status.get('edited_at') as string;
|
||||
|
||||
return (
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
<div
|
||||
onClick={onHeaderClick}
|
||||
onAuxClick={onHeaderClick}
|
||||
{...wrapperProps}
|
||||
className='status__info'
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
>
|
||||
<Link
|
||||
to={`/@${statusAccount?.acct}/${status.get('id') as string}`}
|
||||
className='status__relative-time'
|
||||
>
|
||||
<StatusVisibility visibility={status.get('visibility')} />
|
||||
<RelativeTimestamp timestamp={status.get('created_at') as string} />
|
||||
{editedAt && <StatusEditedAt editedAt={editedAt} />}
|
||||
</Link>
|
||||
|
||||
<StatusDisplayName
|
||||
statusAccount={statusAccount}
|
||||
friendAccount={account}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusVisibility: FC<{ visibility: unknown }> = ({
|
||||
visibility,
|
||||
}) => {
|
||||
if (typeof visibility !== 'string' || !isStatusVisibility(visibility)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className='status__visibility-icon'>
|
||||
<VisibilityIcon visibility={visibility} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const editMessage = defineMessage({
|
||||
id: 'status.edited',
|
||||
defaultMessage: 'Edited {date}',
|
||||
});
|
||||
|
||||
export const StatusEditedAt: FC<{ editedAt: string }> = ({ editedAt }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<abbr
|
||||
title={intl.formatMessage(editMessage, {
|
||||
date: intl.formatDate(editedAt, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{' '}
|
||||
*
|
||||
</abbr>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusDisplayName: FC<{
|
||||
statusAccount?: Account;
|
||||
friendAccount?: Account;
|
||||
avatarSize: number;
|
||||
}> = ({ statusAccount, friendAccount, avatarSize }) => {
|
||||
const AccountComponent = friendAccount ? AvatarOverlay : Avatar;
|
||||
return (
|
||||
<LinkedDisplayName
|
||||
displayProps={{ account: statusAccount }}
|
||||
className='status__display-name'
|
||||
>
|
||||
<div className='status__avatar'>
|
||||
<AccountComponent
|
||||
account={statusAccount}
|
||||
friend={friendAccount}
|
||||
size={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
</LinkedDisplayName>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import { fetchRelationships } from 'mastodon/actions/accounts';
|
||||
import { revealAccount } from 'mastodon/actions/accounts_typed';
|
||||
import { fetchStatus } from 'mastodon/actions/statuses';
|
||||
|
|
@ -18,6 +19,9 @@ import type { RootState } from 'mastodon/store';
|
|||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { Button } from './button';
|
||||
import { IconButton } from './icon_button';
|
||||
import type { StatusHeaderRenderFn } from './status/header';
|
||||
import { StatusHeader } from './status/header';
|
||||
|
||||
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
||||
|
||||
|
|
@ -147,6 +151,11 @@ interface QuotedStatusProps {
|
|||
onQuoteCancel?: () => void; // Used for composer.
|
||||
}
|
||||
|
||||
const quoteCancelMessage = defineMessage({
|
||||
id: 'status.quote.cancel',
|
||||
defaultMessage: 'Cancel quote',
|
||||
});
|
||||
|
||||
export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
quote,
|
||||
contextType,
|
||||
|
|
@ -213,6 +222,22 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
|||
if (accountId && hiddenAccount) dispatch(fetchRelationships([accountId]));
|
||||
}, [accountId, hiddenAccount, dispatch]);
|
||||
|
||||
const intl = useIntl();
|
||||
const headerRenderFn: StatusHeaderRenderFn = useCallback(
|
||||
(props) => (
|
||||
<StatusHeader {...props}>
|
||||
<IconButton
|
||||
onClick={onQuoteCancel}
|
||||
className='status__quote-cancel'
|
||||
title={intl.formatMessage(quoteCancelMessage)}
|
||||
icon='cancel-fill'
|
||||
iconComponent={CancelFillIcon}
|
||||
/>
|
||||
</StatusHeader>
|
||||
),
|
||||
[intl, onQuoteCancel],
|
||||
);
|
||||
|
||||
const isFilteredAndHidden = loadingState === 'filtered';
|
||||
|
||||
let quoteError: React.ReactNode = null;
|
||||
|
|
@ -314,7 +339,7 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
|||
id={quotedStatusId}
|
||||
contextType={contextType}
|
||||
avatarSize={32}
|
||||
onQuoteCancel={onQuoteCancel}
|
||||
headerRenderFn={headerRenderFn}
|
||||
>
|
||||
{canRenderChildQuote && (
|
||||
<QuotedStatus
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user