Add rendering of quote posts in web UI (#34738)
This commit is contained in:
		
					parent
					
						
							
								f1a6f4333a
							
						
					
				
			
			
				commit
				
					
						97b9e8849d
					
				
			
		
					 14 changed files with 219 additions and 43 deletions
				
			
		|  | @ -69,6 +69,10 @@ export function importFetchedStatuses(statuses) { | ||||||
|         processStatus(status.reblog); |         processStatus(status.reblog); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       if (status.quote?.quoted_status) { | ||||||
|  |         processStatus(status.quote.quoted_status); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (status.poll?.id) { |       if (status.poll?.id) { | ||||||
|         pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[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) { | export function normalizeStatus(status, normalOldStatus) { | ||||||
|   const normalStatus   = { ...status }; |   const normalStatus   = { ...status }; | ||||||
|  | 
 | ||||||
|   normalStatus.account = status.account.id; |   normalStatus.account = status.account.id; | ||||||
| 
 | 
 | ||||||
|   if (status.reblog && status.reblog.id) { |   if (status.reblog && status.reblog.id) { | ||||||
|     normalStatus.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) { |   if (status.poll && status.poll.id) { | ||||||
|     normalStatus.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 classNames from 'classnames'; | ||||||
| import { Link } from 'react-router-dom'; | 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 { HotKeys } from 'react-hotkeys'; | import { HotKeys } from 'react-hotkeys'; | ||||||
| 
 | 
 | ||||||
| import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; | 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 RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; | ||||||
| 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'; | ||||||
|  | @ -88,6 +86,7 @@ class Status extends ImmutablePureComponent { | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     status: ImmutablePropTypes.map, |     status: ImmutablePropTypes.map, | ||||||
|     account: ImmutablePropTypes.record, |     account: ImmutablePropTypes.record, | ||||||
|  |     children: PropTypes.node, | ||||||
|     previousId: PropTypes.string, |     previousId: PropTypes.string, | ||||||
|     nextInReplyToId: PropTypes.string, |     nextInReplyToId: PropTypes.string, | ||||||
|     rootId: PropTypes.string, |     rootId: PropTypes.string, | ||||||
|  | @ -115,6 +114,7 @@ class Status extends ImmutablePureComponent { | ||||||
|     onMoveUp: PropTypes.func, |     onMoveUp: PropTypes.func, | ||||||
|     onMoveDown: PropTypes.func, |     onMoveDown: PropTypes.func, | ||||||
|     showThread: PropTypes.bool, |     showThread: PropTypes.bool, | ||||||
|  |     isQuotedPost: PropTypes.bool, | ||||||
|     getScrollPosition: PropTypes.func, |     getScrollPosition: PropTypes.func, | ||||||
|     updateScrollBottom: PropTypes.func, |     updateScrollBottom: PropTypes.func, | ||||||
|     cacheMediaWidth: PropTypes.func, |     cacheMediaWidth: PropTypes.func, | ||||||
|  | @ -372,7 +372,7 @@ class Status extends ImmutablePureComponent { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   render () { |   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; |     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}> |         <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} |           {!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 })} />} |             {(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'> |             <div onClick={this.handleHeaderClick} onAuxClick={this.handleHeaderClick} className='status__info'> | ||||||
|  | @ -576,12 +576,16 @@ class Status extends ImmutablePureComponent { | ||||||
|                   {...statusContentProps} |                   {...statusContentProps} | ||||||
|                 /> |                 /> | ||||||
| 
 | 
 | ||||||
|  |                 {children} | ||||||
|  | 
 | ||||||
|                 {media} |                 {media} | ||||||
|                 {hashtagBar} |                 {hashtagBar} | ||||||
|               </> |               </> | ||||||
|             )} |             )} | ||||||
| 
 | 
 | ||||||
|             <StatusActionBar scrollKey={scrollKey} status={status} account={account}  {...other} /> |             {!isQuotedPost && | ||||||
|  |               <StatusActionBar scrollKey={scrollKey} status={status} account={account}  {...other} /> | ||||||
|  |             } | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </HotKeys> |       </HotKeys> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines'; | ||||||
| import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator'; | import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator'; | ||||||
| import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions'; | 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 { LoadGap } from './load_gap'; | ||||||
| import ScrollableList from './scrollable_list'; | import ScrollableList from './scrollable_list'; | ||||||
|  | @ -113,7 +113,7 @@ export default class StatusList extends ImmutablePureComponent { | ||||||
|           ); |           ); | ||||||
|         default: |         default: | ||||||
|           return ( |           return ( | ||||||
|             <StatusContainer |             <StatusQuoteManager | ||||||
|               key={statusId} |               key={statusId} | ||||||
|               id={statusId} |               id={statusId} | ||||||
|               onMoveUp={this.handleMoveUp} |               onMoveUp={this.handleMoveUp} | ||||||
|  | @ -130,7 +130,7 @@ export default class StatusList extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|     if (scrollableContent && featuredStatusIds) { |     if (scrollableContent && featuredStatusIds) { | ||||||
|       scrollableContent = featuredStatusIds.map(statusId => ( |       scrollableContent = featuredStatusIds.map(statusId => ( | ||||||
|         <StatusContainer |         <StatusQuoteManager | ||||||
|           key={`f-${statusId}`} |           key={`f-${statusId}`} | ||||||
|           id={statusId} |           id={statusId} | ||||||
|           featured |           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 { ColumnBackButton } from 'mastodon/components/column_back_button'; | ||||||
| import { LoadingIndicator } from 'mastodon/components/loading_indicator'; | import { LoadingIndicator } from 'mastodon/components/loading_indicator'; | ||||||
| import { RemoteHint } from 'mastodon/components/remote_hint'; | 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 { AccountHeader } from 'mastodon/features/account_timeline/components/account_header'; | ||||||
| import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; | import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; | ||||||
| import Column from 'mastodon/features/ui/components/column'; | import Column from 'mastodon/features/ui/components/column'; | ||||||
|  | @ -142,9 +142,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ | ||||||
|               /> |               /> | ||||||
|             </h4> |             </h4> | ||||||
|             {featuredStatusIds.map((statusId) => ( |             {featuredStatusIds.map((statusId) => ( | ||||||
|               <StatusContainer |               <StatusQuoteManager | ||||||
|                 key={`f-${statusId}`} |                 key={`f-${statusId}`} | ||||||
|                 // @ts-expect-error inferred props are wrong
 |  | ||||||
|                 id={statusId} |                 id={statusId} | ||||||
|                 contextType='account' |                 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 StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; | ||||||
| import { Account } from 'mastodon/components/account'; | import { Account } from 'mastodon/components/account'; | ||||||
| import { Icon }  from 'mastodon/components/icon'; | 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 { me } from 'mastodon/initial_state'; | ||||||
| import { WithRouterPropTypes } from 'mastodon/utils/react_router'; | import { WithRouterPropTypes } from 'mastodon/utils/react_router'; | ||||||
| 
 | 
 | ||||||
|  | @ -175,7 +175,7 @@ class Notification extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   renderMention (notification) { |   renderMention (notification) { | ||||||
|     return ( |     return ( | ||||||
|       <StatusContainer |       <StatusQuoteManager | ||||||
|         id={notification.get('status')} |         id={notification.get('status')} | ||||||
|         withDismiss |         withDismiss | ||||||
|         hidden={this.props.hidden} |         hidden={this.props.hidden} | ||||||
|  | @ -205,7 +205,7 @@ class Notification extends ImmutablePureComponent { | ||||||
|             </span> |             </span> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <StatusContainer |           <StatusQuoteManager | ||||||
|             id={notification.get('status')} |             id={notification.get('status')} | ||||||
|             account={notification.get('account')} |             account={notification.get('account')} | ||||||
|             muted |             muted | ||||||
|  | @ -235,7 +235,7 @@ class Notification extends ImmutablePureComponent { | ||||||
|             </span> |             </span> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <StatusContainer |           <StatusQuoteManager | ||||||
|             id={notification.get('status')} |             id={notification.get('status')} | ||||||
|             account={notification.get('account')} |             account={notification.get('account')} | ||||||
|             muted |             muted | ||||||
|  | @ -269,7 +269,7 @@ class Notification extends ImmutablePureComponent { | ||||||
|             </span> |             </span> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <StatusContainer |           <StatusQuoteManager | ||||||
|             id={notification.get('status')} |             id={notification.get('status')} | ||||||
|             account={notification.get('account')} |             account={notification.get('account')} | ||||||
|             contextType='notifications' |             contextType='notifications' | ||||||
|  | @ -304,7 +304,7 @@ class Notification extends ImmutablePureComponent { | ||||||
|             </span> |             </span> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <StatusContainer |           <StatusQuoteManager | ||||||
|             id={notification.get('status')} |             id={notification.get('status')} | ||||||
|             account={notification.get('account')} |             account={notification.get('account')} | ||||||
|             contextType='notifications' |             contextType='notifications' | ||||||
|  | @ -345,7 +345,7 @@ class Notification extends ImmutablePureComponent { | ||||||
|             </span> |             </span> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <StatusContainer |           <StatusQuoteManager | ||||||
|             id={notification.get('status')} |             id={notification.get('status')} | ||||||
|             account={account} |             account={account} | ||||||
|             contextType='notifications' |             contextType='notifications' | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import { | ||||||
| } from 'mastodon/actions/statuses'; | } from 'mastodon/actions/statuses'; | ||||||
| import type { IconProp } from 'mastodon/components/icon'; | import type { IconProp } from 'mastodon/components/icon'; | ||||||
| import { Icon } 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 { getStatusHidden } from 'mastodon/selectors/filters'; | ||||||
| import { useAppSelector, useAppDispatch } from 'mastodon/store'; | import { useAppSelector, useAppDispatch } from 'mastodon/store'; | ||||||
| 
 | 
 | ||||||
|  | @ -102,8 +102,7 @@ export const NotificationWithStatus: React.FC<{ | ||||||
|           {label} |           {label} | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <Status |         <StatusQuoteManager | ||||||
|           // @ts-expect-error -- <Status> is not yet typed
 |  | ||||||
|           id={statusId} |           id={statusId} | ||||||
|           contextType='notifications' |           contextType='notifications' | ||||||
|           withDismiss |           withDismiss | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ import { ColumnHeader } from 'mastodon/components/column_header'; | ||||||
| import { CompatibilityHashtag as Hashtag } from 'mastodon/components/hashtag'; | import { CompatibilityHashtag as Hashtag } from 'mastodon/components/hashtag'; | ||||||
| import { Icon } from 'mastodon/components/icon'; | import { Icon } from 'mastodon/components/icon'; | ||||||
| import ScrollableList from 'mastodon/components/scrollable_list'; | 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 { Search } from 'mastodon/features/compose/components/search'; | ||||||
| import { useSearchParam } from 'mastodon/hooks/useSearchParam'; | import { useSearchParam } from 'mastodon/hooks/useSearchParam'; | ||||||
| import type { Hashtag as HashtagType } from 'mastodon/models/tags'; | import type { Hashtag as HashtagType } from 'mastodon/models/tags'; | ||||||
|  | @ -53,8 +53,7 @@ const renderHashtags = (hashtags: HashtagType[]) => | ||||||
| 
 | 
 | ||||||
| const renderStatuses = (statusIds: string[]) => | const renderStatuses = (statusIds: string[]) => | ||||||
|   hidePeek<string>(statusIds).map((id) => ( |   hidePeek<string>(statusIds).map((id) => ( | ||||||
|     // @ts-expect-error inferred props are wrong
 |     <StatusQuoteManager key={id} id={id} /> | ||||||
|     <Status key={id} id={id} /> |  | ||||||
|   )); |   )); | ||||||
| 
 | 
 | ||||||
| type SearchType = 'all' | ApiSearchType; | type SearchType = 'all' | ApiSearchType; | ||||||
|  | @ -190,8 +189,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({ | ||||||
|                   onClickMore={handleSelectStatuses} |                   onClickMore={handleSelectStatuses} | ||||||
|                 > |                 > | ||||||
|                   {results.statuses.slice(0, INITIAL_DISPLAY).map((id) => ( |                   {results.statuses.slice(0, INITIAL_DISPLAY).map((id) => ( | ||||||
|                     // @ts-expect-error inferred props are wrong
 |                     <StatusQuoteManager key={id} id={id} /> | ||||||
|                     <Status key={id} id={id} /> |  | ||||||
|                   ))} |                   ))} | ||||||
|                 </SearchSection> |                 </SearchSection> | ||||||
|               )} |               )} | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ import { IconLogo } from 'mastodon/components/logo'; | ||||||
| import MediaGallery from 'mastodon/components/media_gallery'; | import MediaGallery from 'mastodon/components/media_gallery'; | ||||||
| import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder'; | import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder'; | ||||||
| import StatusContent from 'mastodon/components/status_content'; | import StatusContent from 'mastodon/components/status_content'; | ||||||
|  | import { QuotedStatus } from 'mastodon/components/status_quoted'; | ||||||
| import { VisibilityIcon } from 'mastodon/components/visibility_icon'; | import { VisibilityIcon } from 'mastodon/components/visibility_icon'; | ||||||
| import { Audio } from 'mastodon/features/audio'; | import { Audio } from 'mastodon/features/audio'; | ||||||
| import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task'; | import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task'; | ||||||
|  | @ -371,6 +372,10 @@ export const DetailedStatus: React.FC<{ | ||||||
|               {...(statusContentProps as any)} |               {...(statusContentProps as any)} | ||||||
|             /> |             /> | ||||||
| 
 | 
 | ||||||
|  |             {status.get('quote') && ( | ||||||
|  |               <QuotedStatus quote={status.get('quote')} /> | ||||||
|  |             )} | ||||||
|  | 
 | ||||||
|             {media} |             {media} | ||||||
|             {hashtagBar} |             {hashtagBar} | ||||||
|           </> |           </> | ||||||
|  |  | ||||||
|  | @ -6,8 +6,6 @@ import classNames from 'classnames'; | ||||||
| import { Helmet } from 'react-helmet'; | import { Helmet } from 'react-helmet'; | ||||||
| import { withRouter } from 'react-router-dom'; | import { withRouter } from 'react-router-dom'; | ||||||
| 
 | 
 | ||||||
| import { createSelector } from '@reduxjs/toolkit'; |  | ||||||
| import { List as ImmutableList } from 'immutable'; |  | ||||||
| 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 { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
|  | @ -62,7 +60,7 @@ import { | ||||||
| } from '../../actions/statuses'; | } from '../../actions/statuses'; | ||||||
| import ColumnHeader from '../../components/column_header'; | import ColumnHeader from '../../components/column_header'; | ||||||
| import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; | import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; | ||||||
| import StatusContainer from '../../containers/status_container'; | import { StatusQuoteManager } from '../../components/status_quoted'; | ||||||
| import { deleteModal } from '../../initial_state'; | import { deleteModal } from '../../initial_state'; | ||||||
| import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; | import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; | ||||||
| import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts'; | import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts'; | ||||||
|  | @ -477,7 +475,7 @@ class Status extends ImmutablePureComponent { | ||||||
|     const { params: { statusId } } = this.props; |     const { params: { statusId } } = this.props; | ||||||
| 
 | 
 | ||||||
|     return list.map((id, i) => ( |     return list.map((id, i) => ( | ||||||
|       <StatusContainer |       <StatusQuoteManager | ||||||
|         key={id} |         key={id} | ||||||
|         id={id} |         id={id} | ||||||
|         onMoveUp={this.handleMoveUp} |         onMoveUp={this.handleMoveUp} | ||||||
|  |  | ||||||
|  | @ -863,6 +863,11 @@ | ||||||
|   "status.mute_conversation": "Mute conversation", |   "status.mute_conversation": "Mute conversation", | ||||||
|   "status.open": "Expand this post", |   "status.open": "Expand this post", | ||||||
|   "status.pin": "Feature on profile", |   "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.read_more": "Read more", | ||||||
|   "status.reblog": "Boost", |   "status.reblog": "Boost", | ||||||
|   "status.reblog_private": "Boost with original visibility", |   "status.reblog_private": "Boost with original visibility", | ||||||
|  |  | ||||||
|  | @ -1491,8 +1491,12 @@ body > [data-popper-placement] { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   &--is-quote { | ||||||
|  |     border: none; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   &--in-thread { |   &--in-thread { | ||||||
|     $thread-margin: 46px + 10px; |     --thread-margin: calc(46px + 8px); | ||||||
| 
 | 
 | ||||||
|     border-bottom: 0; |     border-bottom: 0; | ||||||
| 
 | 
 | ||||||
|  | @ -1508,16 +1512,16 @@ body > [data-popper-placement] { | ||||||
|     .hashtag-bar, |     .hashtag-bar, | ||||||
|     .content-warning, |     .content-warning, | ||||||
|     .filter-warning { |     .filter-warning { | ||||||
|       margin-inline-start: $thread-margin; |       margin-inline-start: var(--thread-margin); | ||||||
|       width: calc(100% - $thread-margin); |       width: calc(100% - var(--thread-margin)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .more-from-author { |     .more-from-author { | ||||||
|       width: calc(100% - $thread-margin + 2px); |       width: calc(100% - var(--thread-margin) + 2px); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .status__content__read-more-button { |     .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 { | .detailed-status__link { | ||||||
|   display: inline-flex; |   display: inline-flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|  | @ -2306,11 +2345,6 @@ a.account__display-name { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .status__avatar { |  | ||||||
|   width: 46px; |  | ||||||
|   height: 46px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .muted { | .muted { | ||||||
|   .status__content, |   .status__content, | ||||||
|   .status__content p, |   .status__content p, | ||||||
|  | @ -10515,6 +10549,7 @@ noscript { | ||||||
|       line-height: 22px; |       line-height: 22px; | ||||||
|       color: $darker-text-color; |       color: $darker-text-color; | ||||||
|       -webkit-line-clamp: 4; |       -webkit-line-clamp: 4; | ||||||
|  |       line-clamp: 4; | ||||||
|       -webkit-box-orient: vertical; |       -webkit-box-orient: vertical; | ||||||
|       max-height: none; |       max-height: none; | ||||||
|       overflow: hidden; |       overflow: hidden; | ||||||
|  | @ -10818,9 +10853,9 @@ noscript { | ||||||
| .content-warning { | .content-warning { | ||||||
|   display: block; |   display: block; | ||||||
|   box-sizing: border-box; |   box-sizing: border-box; | ||||||
|   background: rgba($ui-highlight-color, 0.05); |   background: var(--nested-card-background); | ||||||
|   color: $secondary-text-color; |   color: var(--nested-card-text); | ||||||
|   border: 1px solid rgba($ui-highlight-color, 0.15); |   border: var(--nested-card-border); | ||||||
|   border-radius: 8px; |   border-radius: 8px; | ||||||
|   padding: 8px (5px + 8px); |   padding: 8px (5px + 8px); | ||||||
|   position: relative; |   position: relative; | ||||||
|  |  | ||||||
|  | @ -27,6 +27,10 @@ | ||||||
|   --rich-text-container-color: rgba(87, 24, 60, 100%); |   --rich-text-container-color: rgba(87, 24, 60, 100%); | ||||||
|   --rich-text-text-color: rgba(255, 175, 212, 100%); |   --rich-text-text-color: rgba(255, 175, 212, 100%); | ||||||
|   --rich-text-decorations-color: rgba(128, 58, 95, 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-placeholder-color: #{$dark-text-color}; | ||||||
|   --input-background-color: var(--surface-variant-background-color); |   --input-background-color: var(--surface-variant-background-color); | ||||||
|   --on-input-color: #{$secondary-text-color}; |   --on-input-color: #{$secondary-text-color}; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue