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 { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.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 { Hotkeys } from 'mastodon/components/hotkeys';
|
||||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||||
import { FilterWarning } from 'mastodon/components/filter_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 { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
|
||||||
import { displayMedia } from '../initial_state';
|
import { displayMedia } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { StatusHeader } from './status/header'
|
||||||
import { AvatarOverlay } from './avatar_overlay';
|
|
||||||
import { LinkedDisplayName } from './display_name';
|
import { LinkedDisplayName } from './display_name';
|
||||||
import { getHashtagBarForStatus } from './hashtag_bar';
|
import { getHashtagBarForStatus } from './hashtag_bar';
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import { StatusThreadLabel } from './status_thread_label';
|
import { StatusThreadLabel } from './status_thread_label';
|
||||||
import { VisibilityIcon } from './visibility_icon';
|
|
||||||
import { IconButton } from './icon_button';
|
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
@ -112,7 +106,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
onToggleCollapsed: PropTypes.func,
|
onToggleCollapsed: PropTypes.func,
|
||||||
onTranslate: PropTypes.func,
|
onTranslate: PropTypes.func,
|
||||||
onInteractionModal: PropTypes.func,
|
onInteractionModal: PropTypes.func,
|
||||||
onQuoteCancel: PropTypes.func,
|
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
@ -129,6 +122,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
avatarSize: PropTypes.number,
|
avatarSize: PropTypes.number,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
unfocusable: PropTypes.bool,
|
unfocusable: PropTypes.bool,
|
||||||
|
headerRenderFn: PropTypes.func,
|
||||||
pictureInPicture: ImmutablePropTypes.contains({
|
pictureInPicture: ImmutablePropTypes.contains({
|
||||||
inUse: PropTypes.bool,
|
inUse: PropTypes.bool,
|
||||||
available: PropTypes.bool,
|
available: PropTypes.bool,
|
||||||
@ -146,7 +140,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
'hidden',
|
'hidden',
|
||||||
'unread',
|
'unread',
|
||||||
'pictureInPicture',
|
'pictureInPicture',
|
||||||
'onQuoteCancel',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -364,10 +357,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
|
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleQuoteCancel = () => {
|
|
||||||
this.props.onQuoteCancel?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
_properStatus () {
|
_properStatus () {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
|
|
||||||
@ -383,7 +372,24 @@ class Status extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
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;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
@ -405,7 +411,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
onTranslate: this.handleTranslate,
|
onTranslate: this.handleTranslate,
|
||||||
};
|
};
|
||||||
|
|
||||||
let media, statusAvatar, prepend, rebloggedByText;
|
let media, prepend, rebloggedByText;
|
||||||
|
|
||||||
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
||||||
const connectToRoot = rootId && rootId === 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 {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 (
|
return (
|
||||||
<Hotkeys handlers={handlers} focusable={!unfocusable}>
|
<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}>
|
<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 })} />}
|
{(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'>
|
{header}
|
||||||
<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>
|
|
||||||
|
|
||||||
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
{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 { 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 type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||||
import { fetchRelationships } from 'mastodon/actions/accounts';
|
import { fetchRelationships } from 'mastodon/actions/accounts';
|
||||||
import { revealAccount } from 'mastodon/actions/accounts_typed';
|
import { revealAccount } from 'mastodon/actions/accounts_typed';
|
||||||
import { fetchStatus } from 'mastodon/actions/statuses';
|
import { fetchStatus } from 'mastodon/actions/statuses';
|
||||||
@ -18,6 +19,9 @@ import type { RootState } from 'mastodon/store';
|
|||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { Button } from './button';
|
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;
|
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
||||||
|
|
||||||
@ -147,6 +151,11 @@ interface QuotedStatusProps {
|
|||||||
onQuoteCancel?: () => void; // Used for composer.
|
onQuoteCancel?: () => void; // Used for composer.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quoteCancelMessage = defineMessage({
|
||||||
|
id: 'status.quote.cancel',
|
||||||
|
defaultMessage: 'Cancel quote',
|
||||||
|
});
|
||||||
|
|
||||||
export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||||
quote,
|
quote,
|
||||||
contextType,
|
contextType,
|
||||||
@ -213,6 +222,22 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
|||||||
if (accountId && hiddenAccount) dispatch(fetchRelationships([accountId]));
|
if (accountId && hiddenAccount) dispatch(fetchRelationships([accountId]));
|
||||||
}, [accountId, hiddenAccount, dispatch]);
|
}, [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';
|
const isFilteredAndHidden = loadingState === 'filtered';
|
||||||
|
|
||||||
let quoteError: React.ReactNode = null;
|
let quoteError: React.ReactNode = null;
|
||||||
@ -314,7 +339,7 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
|||||||
id={quotedStatusId}
|
id={quotedStatusId}
|
||||||
contextType={contextType}
|
contextType={contextType}
|
||||||
avatarSize={32}
|
avatarSize={32}
|
||||||
onQuoteCancel={onQuoteCancel}
|
headerRenderFn={headerRenderFn}
|
||||||
>
|
>
|
||||||
{canRenderChildQuote && (
|
{canRenderChildQuote && (
|
||||||
<QuotedStatus
|
<QuotedStatus
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user