Add rendering of quote posts in web UI (#34738)
This commit is contained in:
		@@ -69,6 +69,10 @@ export function importFetchedStatuses(statuses) {
 | 
			
		||||
        processStatus(status.reblog);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (status.quote?.quoted_status) {
 | 
			
		||||
        processStatus(status.quote.quoted_status);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (status.poll?.id) {
 | 
			
		||||
        pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id]));
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,12 +23,20 @@ export function normalizeFilterResult(result) {
 | 
			
		||||
 | 
			
		||||
export function normalizeStatus(status, normalOldStatus) {
 | 
			
		||||
  const normalStatus   = { ...status };
 | 
			
		||||
 | 
			
		||||
  normalStatus.account = status.account.id;
 | 
			
		||||
 | 
			
		||||
  if (status.reblog && status.reblog.id) {
 | 
			
		||||
    normalStatus.reblog = status.reblog.id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (status.quote?.quoted_status ?? status.quote?.quoted_status_id) {
 | 
			
		||||
    normalStatus.quote = {
 | 
			
		||||
      ...status.quote,
 | 
			
		||||
      quoted_status: status.quote.quoted_status?.id ?? status.quote?.quoted_status_id,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (status.poll && status.poll.id) {
 | 
			
		||||
    normalStatus.poll = status.poll.id;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,12 @@ 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 { HotKeys } from 'react-hotkeys';
 | 
			
		||||
 | 
			
		||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 | 
			
		||||
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
 | 
			
		||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
 | 
			
		||||
import { ContentWarning } from 'mastodon/components/content_warning';
 | 
			
		||||
import { FilterWarning } from 'mastodon/components/filter_warning';
 | 
			
		||||
@@ -88,6 +86,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    status: ImmutablePropTypes.map,
 | 
			
		||||
    account: ImmutablePropTypes.record,
 | 
			
		||||
    children: PropTypes.node,
 | 
			
		||||
    previousId: PropTypes.string,
 | 
			
		||||
    nextInReplyToId: PropTypes.string,
 | 
			
		||||
    rootId: PropTypes.string,
 | 
			
		||||
@@ -115,6 +114,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    onMoveUp: PropTypes.func,
 | 
			
		||||
    onMoveDown: PropTypes.func,
 | 
			
		||||
    showThread: PropTypes.bool,
 | 
			
		||||
    isQuotedPost: PropTypes.bool,
 | 
			
		||||
    getScrollPosition: PropTypes.func,
 | 
			
		||||
    updateScrollBottom: PropTypes.func,
 | 
			
		||||
    cacheMediaWidth: PropTypes.func,
 | 
			
		||||
@@ -372,7 +372,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
 | 
			
		||||
    const { intl, hidden, featured, unfocusable, unread, showThread, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
 | 
			
		||||
 | 
			
		||||
    let { status, account, ...other } = this.props;
 | 
			
		||||
 | 
			
		||||
@@ -543,7 +543,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
        <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)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
 | 
			
		||||
          {!skipPrepend && prepend}
 | 
			
		||||
 | 
			
		||||
          <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
 | 
			
		||||
          <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'status--is-quote': isQuotedPost })} data-id={status.get('id')}>
 | 
			
		||||
            {(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'>
 | 
			
		||||
@@ -576,12 +576,16 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                  {...statusContentProps}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                {children}
 | 
			
		||||
 | 
			
		||||
                {media}
 | 
			
		||||
                {hashtagBar}
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <StatusActionBar scrollKey={scrollKey} status={status} account={account}  {...other} />
 | 
			
		||||
            {!isQuotedPost &&
 | 
			
		||||
              <StatusActionBar scrollKey={scrollKey} status={status} account={account}  {...other} />
 | 
			
		||||
            }
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </HotKeys>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines';
 | 
			
		||||
import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator';
 | 
			
		||||
import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions';
 | 
			
		||||
 | 
			
		||||
import StatusContainer from '../containers/status_container';
 | 
			
		||||
import { StatusQuoteManager } from '../components/status_quoted';
 | 
			
		||||
 | 
			
		||||
import { LoadGap } from './load_gap';
 | 
			
		||||
import ScrollableList from './scrollable_list';
 | 
			
		||||
@@ -113,7 +113,7 @@ export default class StatusList extends ImmutablePureComponent {
 | 
			
		||||
          );
 | 
			
		||||
        default:
 | 
			
		||||
          return (
 | 
			
		||||
            <StatusContainer
 | 
			
		||||
            <StatusQuoteManager
 | 
			
		||||
              key={statusId}
 | 
			
		||||
              id={statusId}
 | 
			
		||||
              onMoveUp={this.handleMoveUp}
 | 
			
		||||
@@ -130,7 +130,7 @@ export default class StatusList extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
    if (scrollableContent && featuredStatusIds) {
 | 
			
		||||
      scrollableContent = featuredStatusIds.map(statusId => (
 | 
			
		||||
        <StatusContainer
 | 
			
		||||
        <StatusQuoteManager
 | 
			
		||||
          key={`f-${statusId}`}
 | 
			
		||||
          id={statusId}
 | 
			
		||||
          featured
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										117
									
								
								app/javascript/mastodon/components/status_quoted.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								app/javascript/mastodon/components/status_quoted.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import type { Map as ImmutableMap } from 'immutable';
 | 
			
		||||
 | 
			
		||||
import { Icon } from 'mastodon/components/icon';
 | 
			
		||||
import StatusContainer from 'mastodon/containers/status_container';
 | 
			
		||||
import { useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
import QuoteIcon from '../../images/quote.svg?react';
 | 
			
		||||
 | 
			
		||||
const QuoteWrapper: React.FC<{
 | 
			
		||||
  isError?: boolean;
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}> = ({ isError, children }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames('status__quote', {
 | 
			
		||||
        'status__quote--error': isError,
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      <Icon id='quote' icon={QuoteIcon} className='status__quote-icon' />
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
 | 
			
		||||
 | 
			
		||||
export const QuotedStatus: React.FC<{ quote: QuoteMap }> = ({ quote }) => {
 | 
			
		||||
  const quotedStatusId = quote.get('quoted_status');
 | 
			
		||||
  const state = quote.get('state');
 | 
			
		||||
  const status = useAppSelector((state) =>
 | 
			
		||||
    quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  let quoteError: React.ReactNode | null = null;
 | 
			
		||||
 | 
			
		||||
  if (state === 'deleted') {
 | 
			
		||||
    quoteError = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='status.quote_error.removed'
 | 
			
		||||
        defaultMessage='This post was removed by its author.'
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  } else if (state === 'unauthorized') {
 | 
			
		||||
    quoteError = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='status.quote_error.unauthorized'
 | 
			
		||||
        defaultMessage='This post cannot be displayed as you are not authorized to view it.'
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  } else if (state === 'pending') {
 | 
			
		||||
    quoteError = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='status.quote_error.pending_approval'
 | 
			
		||||
        defaultMessage='This post is pending approval from the original author.'
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  } else if (state === 'rejected' || state === 'revoked') {
 | 
			
		||||
    quoteError = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='status.quote_error.rejected'
 | 
			
		||||
        defaultMessage='This post cannot be displayed as the original author does not allow it to be quoted.'
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  } else if (!status || !quotedStatusId) {
 | 
			
		||||
    quoteError = (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='status.quote_error.not_found'
 | 
			
		||||
        defaultMessage='This post cannot be displayed.'
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (quoteError) {
 | 
			
		||||
    return <QuoteWrapper isError>{quoteError}</QuoteWrapper>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <QuoteWrapper>
 | 
			
		||||
      <StatusContainer
 | 
			
		||||
        // @ts-expect-error Status isn't typed yet
 | 
			
		||||
        isQuotedPost
 | 
			
		||||
        id={quotedStatusId}
 | 
			
		||||
        avatarSize={40}
 | 
			
		||||
      />
 | 
			
		||||
    </QuoteWrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface StatusQuoteManagerProps {
 | 
			
		||||
  id: string;
 | 
			
		||||
  [key: string]: unknown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This wrapper component takes a status ID and, if the associated status
 | 
			
		||||
 * is a quote post, it renders the quote into `StatusContainer` as a child.
 | 
			
		||||
 * It passes all other props through to `StatusContainer`.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
 | 
			
		||||
  const status = useAppSelector((state) => state.statuses.get(props.id));
 | 
			
		||||
  const quote = status?.get('quote') as QuoteMap | undefined;
 | 
			
		||||
 | 
			
		||||
  if (quote) {
 | 
			
		||||
    return (
 | 
			
		||||
      <StatusContainer {...props}>
 | 
			
		||||
        <QuotedStatus quote={quote} />
 | 
			
		||||
      </StatusContainer>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <StatusContainer {...props} />;
 | 
			
		||||
};
 | 
			
		||||
@@ -14,7 +14,7 @@ import { Account } from 'mastodon/components/account';
 | 
			
		||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
 | 
			
		||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 | 
			
		||||
import { RemoteHint } from 'mastodon/components/remote_hint';
 | 
			
		||||
import StatusContainer from 'mastodon/containers/status_container';
 | 
			
		||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
 | 
			
		||||
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
 | 
			
		||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 | 
			
		||||
import Column from 'mastodon/features/ui/components/column';
 | 
			
		||||
@@ -142,9 +142,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
 | 
			
		||||
              />
 | 
			
		||||
            </h4>
 | 
			
		||||
            {featuredStatusIds.map((statusId) => (
 | 
			
		||||
              <StatusContainer
 | 
			
		||||
              <StatusQuoteManager
 | 
			
		||||
                key={`f-${statusId}`}
 | 
			
		||||
                // @ts-expect-error inferred props are wrong
 | 
			
		||||
                id={statusId}
 | 
			
		||||
                contextType='account'
 | 
			
		||||
              />
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
 | 
			
		||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
 | 
			
		||||
import { Account } from 'mastodon/components/account';
 | 
			
		||||
import { Icon }  from 'mastodon/components/icon';
 | 
			
		||||
import StatusContainer from 'mastodon/containers/status_container';
 | 
			
		||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
 | 
			
		||||
import { me } from 'mastodon/initial_state';
 | 
			
		||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
 | 
			
		||||
 | 
			
		||||
@@ -175,7 +175,7 @@ class Notification extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  renderMention (notification) {
 | 
			
		||||
    return (
 | 
			
		||||
      <StatusContainer
 | 
			
		||||
      <StatusQuoteManager
 | 
			
		||||
        id={notification.get('status')}
 | 
			
		||||
        withDismiss
 | 
			
		||||
        hidden={this.props.hidden}
 | 
			
		||||
@@ -205,7 +205,7 @@ class Notification extends ImmutablePureComponent {
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <StatusContainer
 | 
			
		||||
          <StatusQuoteManager
 | 
			
		||||
            id={notification.get('status')}
 | 
			
		||||
            account={notification.get('account')}
 | 
			
		||||
            muted
 | 
			
		||||
@@ -235,7 +235,7 @@ class Notification extends ImmutablePureComponent {
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <StatusContainer
 | 
			
		||||
          <StatusQuoteManager
 | 
			
		||||
            id={notification.get('status')}
 | 
			
		||||
            account={notification.get('account')}
 | 
			
		||||
            muted
 | 
			
		||||
@@ -269,7 +269,7 @@ class Notification extends ImmutablePureComponent {
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <StatusContainer
 | 
			
		||||
          <StatusQuoteManager
 | 
			
		||||
            id={notification.get('status')}
 | 
			
		||||
            account={notification.get('account')}
 | 
			
		||||
            contextType='notifications'
 | 
			
		||||
@@ -304,7 +304,7 @@ class Notification extends ImmutablePureComponent {
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <StatusContainer
 | 
			
		||||
          <StatusQuoteManager
 | 
			
		||||
            id={notification.get('status')}
 | 
			
		||||
            account={notification.get('account')}
 | 
			
		||||
            contextType='notifications'
 | 
			
		||||
@@ -345,7 +345,7 @@ class Notification extends ImmutablePureComponent {
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <StatusContainer
 | 
			
		||||
          <StatusQuoteManager
 | 
			
		||||
            id={notification.get('status')}
 | 
			
		||||
            account={account}
 | 
			
		||||
            contextType='notifications'
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import {
 | 
			
		||||
} from 'mastodon/actions/statuses';
 | 
			
		||||
import type { IconProp } from 'mastodon/components/icon';
 | 
			
		||||
import { Icon } from 'mastodon/components/icon';
 | 
			
		||||
import Status from 'mastodon/containers/status_container';
 | 
			
		||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
 | 
			
		||||
import { getStatusHidden } from 'mastodon/selectors/filters';
 | 
			
		||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
@@ -102,8 +102,7 @@ export const NotificationWithStatus: React.FC<{
 | 
			
		||||
          {label}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Status
 | 
			
		||||
          // @ts-expect-error -- <Status> is not yet typed
 | 
			
		||||
        <StatusQuoteManager
 | 
			
		||||
          id={statusId}
 | 
			
		||||
          contextType='notifications'
 | 
			
		||||
          withDismiss
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import { ColumnHeader } from 'mastodon/components/column_header';
 | 
			
		||||
import { CompatibilityHashtag as Hashtag } from 'mastodon/components/hashtag';
 | 
			
		||||
import { Icon } from 'mastodon/components/icon';
 | 
			
		||||
import ScrollableList from 'mastodon/components/scrollable_list';
 | 
			
		||||
import Status from 'mastodon/containers/status_container';
 | 
			
		||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
 | 
			
		||||
import { Search } from 'mastodon/features/compose/components/search';
 | 
			
		||||
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
 | 
			
		||||
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
 | 
			
		||||
@@ -53,8 +53,7 @@ const renderHashtags = (hashtags: HashtagType[]) =>
 | 
			
		||||
 | 
			
		||||
const renderStatuses = (statusIds: string[]) =>
 | 
			
		||||
  hidePeek<string>(statusIds).map((id) => (
 | 
			
		||||
    // @ts-expect-error inferred props are wrong
 | 
			
		||||
    <Status key={id} id={id} />
 | 
			
		||||
    <StatusQuoteManager key={id} id={id} />
 | 
			
		||||
  ));
 | 
			
		||||
 | 
			
		||||
type SearchType = 'all' | ApiSearchType;
 | 
			
		||||
@@ -190,8 +189,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
 | 
			
		||||
                  onClickMore={handleSelectStatuses}
 | 
			
		||||
                >
 | 
			
		||||
                  {results.statuses.slice(0, INITIAL_DISPLAY).map((id) => (
 | 
			
		||||
                    // @ts-expect-error inferred props are wrong
 | 
			
		||||
                    <Status key={id} id={id} />
 | 
			
		||||
                    <StatusQuoteManager key={id} id={id} />
 | 
			
		||||
                  ))}
 | 
			
		||||
                </SearchSection>
 | 
			
		||||
              )}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import { IconLogo } from 'mastodon/components/logo';
 | 
			
		||||
import MediaGallery from 'mastodon/components/media_gallery';
 | 
			
		||||
import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder';
 | 
			
		||||
import StatusContent from 'mastodon/components/status_content';
 | 
			
		||||
import { QuotedStatus } from 'mastodon/components/status_quoted';
 | 
			
		||||
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
 | 
			
		||||
import { Audio } from 'mastodon/features/audio';
 | 
			
		||||
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
 | 
			
		||||
@@ -371,6 +372,10 @@ export const DetailedStatus: React.FC<{
 | 
			
		||||
              {...(statusContentProps as any)}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            {status.get('quote') && (
 | 
			
		||||
              <QuotedStatus quote={status.get('quote')} />
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            {media}
 | 
			
		||||
            {hashtagBar}
 | 
			
		||||
          </>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,6 @@ import classNames from 'classnames';
 | 
			
		||||
import { Helmet } from 'react-helmet';
 | 
			
		||||
import { withRouter } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import { createSelector } from '@reduxjs/toolkit';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
@@ -62,7 +60,7 @@ import {
 | 
			
		||||
} from '../../actions/statuses';
 | 
			
		||||
import ColumnHeader from '../../components/column_header';
 | 
			
		||||
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
 | 
			
		||||
import StatusContainer from '../../containers/status_container';
 | 
			
		||||
import { StatusQuoteManager } from '../../components/status_quoted';
 | 
			
		||||
import { deleteModal } from '../../initial_state';
 | 
			
		||||
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
 | 
			
		||||
import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts';
 | 
			
		||||
@@ -477,7 +475,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    const { params: { statusId } } = this.props;
 | 
			
		||||
 | 
			
		||||
    return list.map((id, i) => (
 | 
			
		||||
      <StatusContainer
 | 
			
		||||
      <StatusQuoteManager
 | 
			
		||||
        key={id}
 | 
			
		||||
        id={id}
 | 
			
		||||
        onMoveUp={this.handleMoveUp}
 | 
			
		||||
 
 | 
			
		||||
@@ -863,6 +863,11 @@
 | 
			
		||||
  "status.mute_conversation": "Mute conversation",
 | 
			
		||||
  "status.open": "Expand this post",
 | 
			
		||||
  "status.pin": "Feature on profile",
 | 
			
		||||
  "status.quote_error.not_found": "This post cannot be displayed.",
 | 
			
		||||
  "status.quote_error.pending_approval": "This post is pending approval from the original author.",
 | 
			
		||||
  "status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
 | 
			
		||||
  "status.quote_error.removed": "This post was removed by its author.",
 | 
			
		||||
  "status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.",
 | 
			
		||||
  "status.read_more": "Read more",
 | 
			
		||||
  "status.reblog": "Boost",
 | 
			
		||||
  "status.reblog_private": "Boost with original visibility",
 | 
			
		||||
 
 | 
			
		||||
@@ -1491,8 +1491,12 @@ body > [data-popper-placement] {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--is-quote {
 | 
			
		||||
    border: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--in-thread {
 | 
			
		||||
    $thread-margin: 46px + 10px;
 | 
			
		||||
    --thread-margin: calc(46px + 8px);
 | 
			
		||||
 | 
			
		||||
    border-bottom: 0;
 | 
			
		||||
 | 
			
		||||
@@ -1508,16 +1512,16 @@ body > [data-popper-placement] {
 | 
			
		||||
    .hashtag-bar,
 | 
			
		||||
    .content-warning,
 | 
			
		||||
    .filter-warning {
 | 
			
		||||
      margin-inline-start: $thread-margin;
 | 
			
		||||
      width: calc(100% - $thread-margin);
 | 
			
		||||
      margin-inline-start: var(--thread-margin);
 | 
			
		||||
      width: calc(100% - var(--thread-margin));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .more-from-author {
 | 
			
		||||
      width: calc(100% - $thread-margin + 2px);
 | 
			
		||||
      width: calc(100% - var(--thread-margin) + 2px);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .status__content__read-more-button {
 | 
			
		||||
      margin-inline-start: $thread-margin;
 | 
			
		||||
      margin-inline-start: var(--thread-margin);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1873,6 +1877,41 @@ body > [data-popper-placement] {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status__quote {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin-block-start: 16px;
 | 
			
		||||
  margin-inline-start: 56px;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  color: var(--nested-card-text);
 | 
			
		||||
  background: var(--nested-card-background);
 | 
			
		||||
  border: var(--nested-card-border);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status__quote--error {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
  padding: 12px;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status__quote-icon {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  inset-block-start: 18px;
 | 
			
		||||
  inset-inline-start: -50px;
 | 
			
		||||
  display: block;
 | 
			
		||||
  width: 26px;
 | 
			
		||||
  height: 26px;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  color: #6a49ba;
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
 | 
			
		||||
  .status__quote--error & {
 | 
			
		||||
    inset-block-start: 50%;
 | 
			
		||||
    transform: translateY(-50%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.detailed-status__link {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
@@ -2306,11 +2345,6 @@ a.account__display-name {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status__avatar {
 | 
			
		||||
  width: 46px;
 | 
			
		||||
  height: 46px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.muted {
 | 
			
		||||
  .status__content,
 | 
			
		||||
  .status__content p,
 | 
			
		||||
@@ -10515,6 +10549,7 @@ noscript {
 | 
			
		||||
      line-height: 22px;
 | 
			
		||||
      color: $darker-text-color;
 | 
			
		||||
      -webkit-line-clamp: 4;
 | 
			
		||||
      line-clamp: 4;
 | 
			
		||||
      -webkit-box-orient: vertical;
 | 
			
		||||
      max-height: none;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
@@ -10818,9 +10853,9 @@ noscript {
 | 
			
		||||
.content-warning {
 | 
			
		||||
  display: block;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  background: rgba($ui-highlight-color, 0.05);
 | 
			
		||||
  color: $secondary-text-color;
 | 
			
		||||
  border: 1px solid rgba($ui-highlight-color, 0.15);
 | 
			
		||||
  background: var(--nested-card-background);
 | 
			
		||||
  color: var(--nested-card-text);
 | 
			
		||||
  border: var(--nested-card-border);
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  padding: 8px (5px + 8px);
 | 
			
		||||
  position: relative;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,10 @@
 | 
			
		||||
  --rich-text-container-color: rgba(87, 24, 60, 100%);
 | 
			
		||||
  --rich-text-text-color: rgba(255, 175, 212, 100%);
 | 
			
		||||
  --rich-text-decorations-color: rgba(128, 58, 95, 100%);
 | 
			
		||||
  --nested-card-background: color(from #{$ui-highlight-color} srgb r g b / 5%);
 | 
			
		||||
  --nested-card-text: #{$secondary-text-color};
 | 
			
		||||
  --nested-card-border: 1px solid
 | 
			
		||||
    color(from #{$ui-highlight-color} srgb r g b / 15%);
 | 
			
		||||
  --input-placeholder-color: #{$dark-text-color};
 | 
			
		||||
  --input-background-color: var(--surface-variant-background-color);
 | 
			
		||||
  --on-input-color: #{$secondary-text-color};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user