diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 8d7d689a6aa..59f6f7d07eb 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -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 = ; - } else { - statusAvatar = ; - } - const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); + + const header = this.props.headerRenderFn + ? this.props.headerRenderFn({ status, account, avatarSize, messages, onHeaderClick: this.handleHeaderClick }) + : ( + + ); + return (
@@ -575,28 +587,7 @@ class Status extends ImmutablePureComponent { > {(connectReply || connectUp || connectToRoot) &&
} -
- - - {status.get('edited_at') && *} - - - -
- {statusAvatar} -
-
- - {isQuotedPost && !!this.props.onQuoteCancel && ( - - )} -
+ {header} {matchedFilters && } diff --git a/app/javascript/mastodon/components/status/header.tsx b/app/javascript/mastodon/components/status/header.tsx new file mode 100644 index 00000000000..3416c39dd29 --- /dev/null +++ b/app/javascript/mastodon/components/status/header.tsx @@ -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; + displayNameProps?: DisplayNameProps; + onHeaderClick?: MouseEventHandler; +} + +export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode; + +export const StatusHeader: FC = ({ + 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 */ +
+ + + + {editedAt && } + + + + + {children} +
+ ); +}; + +export const StatusVisibility: FC<{ visibility: unknown }> = ({ + visibility, +}) => { + if (typeof visibility !== 'string' || !isStatusVisibility(visibility)) { + return null; + } + return ( + + + + ); +}; + +const editMessage = defineMessage({ + id: 'status.edited', + defaultMessage: 'Edited {date}', +}); + +export const StatusEditedAt: FC<{ editedAt: string }> = ({ editedAt }) => { + const intl = useIntl(); + return ( + + {' '} + * + + ); +}; + +export const StatusDisplayName: FC<{ + statusAccount?: Account; + friendAccount?: Account; + avatarSize: number; +}> = ({ statusAccount, friendAccount, avatarSize }) => { + const AccountComponent = friendAccount ? AvatarOverlay : Avatar; + return ( + +
+ +
+
+ ); +}; diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index 33e791a548b..8effec874f2 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -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 = ({ quote, contextType, @@ -213,6 +222,22 @@ export const QuotedStatus: React.FC = ({ if (accountId && hiddenAccount) dispatch(fetchRelationships([accountId])); }, [accountId, hiddenAccount, dispatch]); + const intl = useIntl(); + const headerRenderFn: StatusHeaderRenderFn = useCallback( + (props) => ( + + + + ), + [intl, onQuoteCancel], + ); + const isFilteredAndHidden = loadingState === 'filtered'; let quoteError: React.ReactNode = null; @@ -314,7 +339,7 @@ export const QuotedStatus: React.FC = ({ id={quotedStatusId} contextType={contextType} avatarSize={32} - onQuoteCancel={onQuoteCancel} + headerRenderFn={headerRenderFn} > {canRenderChildQuote && (