Include preview cards in status entity in REST API (#9120)
* Include preview cards in status entity in REST API * Display preview card in-stream * Improve in-stream display of preview cards
This commit is contained in:
		
							parent
							
								
									f435af86e7
								
							
						
					
					
						commit
						cf2ab9c394
					
				
					 10 changed files with 61 additions and 11 deletions
				
			
		|  | @ -126,6 +126,7 @@ class ApplicationController < ActionController::Base | |||
|   def respond_with_error(code) | ||||
|     respond_to do |format| | ||||
|       format.any  { head code } | ||||
| 
 | ||||
|       format.html do | ||||
|         set_locale | ||||
|         render "errors/#{code}", layout: 'error', status: code | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import DisplayName from './display_name'; | |||
| import StatusContent from './status_content'; | ||||
| import StatusActionBar from './status_action_bar'; | ||||
| import AttachmentList from './attachment_list'; | ||||
| import Card from '../features/status/components/card'; | ||||
| import { injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import { MediaGallery, Video } from '../features/ui/util/async-components'; | ||||
|  | @ -256,6 +257,14 @@ class Status extends ImmutablePureComponent { | |||
|           </Bundle> | ||||
|         ); | ||||
|       } | ||||
|     } else if (status.get('spoiler_text').length === 0 && status.get('card')) { | ||||
|       media = ( | ||||
|         <Card | ||||
|           onOpenMedia={this.props.onOpenMedia} | ||||
|           card={status.get('card')} | ||||
|           compact | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (otherAccounts) { | ||||
|  |  | |||
|  | @ -59,10 +59,12 @@ export default class Card extends React.PureComponent { | |||
|     card: ImmutablePropTypes.map, | ||||
|     maxDescription: PropTypes.number, | ||||
|     onOpenMedia: PropTypes.func.isRequired, | ||||
|     compact: PropTypes.boolean, | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     maxDescription: 50, | ||||
|     compact: false, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|  | @ -131,7 +133,7 @@ export default class Card extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { card, maxDescription } = this.props; | ||||
|     const { card, maxDescription, compact } = this.props; | ||||
|     const { width, embedded } = this.state; | ||||
| 
 | ||||
|     if (card === null) { | ||||
|  | @ -139,17 +141,17 @@ export default class Card extends React.PureComponent { | |||
|     } | ||||
| 
 | ||||
|     const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); | ||||
|     const horizontal  = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link'; | ||||
|     const className   = classnames('status-card', { horizontal }); | ||||
|     const horizontal  = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; | ||||
|     const interactive = card.get('type') !== 'link'; | ||||
|     const className   = classnames('status-card', { horizontal, compact, interactive }); | ||||
|     const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; | ||||
|     const ratio       = card.get('width') / card.get('height'); | ||||
|     const ratio       = compact ? 16 / 9 : card.get('width') / card.get('height'); | ||||
|     const height      = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); | ||||
| 
 | ||||
|     const description = ( | ||||
|       <div className='status-card__content'> | ||||
|         {title} | ||||
|         {!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} | ||||
|         {!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} | ||||
|         <span className='status-card__host'>{provider}</span> | ||||
|       </div> | ||||
|     ); | ||||
|  | @ -174,7 +176,7 @@ export default class Card extends React.PureComponent { | |||
|             <div className='status-card__actions'> | ||||
|               <div> | ||||
|                 <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button> | ||||
|                 <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a> | ||||
|                 {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>} | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|  | @ -184,7 +186,7 @@ export default class Card extends React.PureComponent { | |||
|       return ( | ||||
|         <div className={className} ref={this.setRef}> | ||||
|           {embed} | ||||
|           {description} | ||||
|           {!compact && description} | ||||
|         </div> | ||||
|       ); | ||||
|     } else if (card.get('image')) { | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import { connect } from 'react-redux'; | |||
| import Card from '../components/card'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { statusId }) => ({ | ||||
|   card: state.getIn(['cards', statusId], null), | ||||
|   card: state.getIn(['statuses', statusId, 'card'], null), | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps)(Card); | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ import relationships from './relationships'; | |||
| import settings from './settings'; | ||||
| import push_notifications from './push_notifications'; | ||||
| import status_lists from './status_lists'; | ||||
| import cards from './cards'; | ||||
| import mutes from './mutes'; | ||||
| import reports from './reports'; | ||||
| import contexts from './contexts'; | ||||
|  | @ -46,7 +45,6 @@ const reducers = { | |||
|   relationships, | ||||
|   settings, | ||||
|   push_notifications, | ||||
|   cards, | ||||
|   mutes, | ||||
|   reports, | ||||
|   contexts, | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import { | |||
|   STATUS_REVEAL, | ||||
|   STATUS_HIDE, | ||||
| } from '../actions/statuses'; | ||||
| import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; | ||||
| import { TIMELINE_DELETE } from '../actions/timelines'; | ||||
| import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; | ||||
| import { Map as ImmutableMap, fromJS } from 'immutable'; | ||||
|  | @ -65,6 +66,8 @@ export default function statuses(state = initialState, action) { | |||
|     }); | ||||
|   case TIMELINE_DELETE: | ||||
|     return deleteStatus(state, action.id, action.references); | ||||
|   case STATUS_CARD_FETCH_SUCCESS: | ||||
|     return state.setIn([action.id, 'card'], fromJS(action.card)); | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
|  |  | |||
|  | @ -2560,6 +2560,9 @@ a.status-card { | |||
|   display: block; | ||||
|   margin-top: 5px; | ||||
|   font-size: 13px; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .status-card__image { | ||||
|  | @ -2584,6 +2587,31 @@ a.status-card { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .status-card.compact { | ||||
|   border-color: lighten($ui-base-color, 4%); | ||||
| 
 | ||||
|   &.interactive { | ||||
|     border: 0; | ||||
|   } | ||||
| 
 | ||||
|   .status-card__content { | ||||
|     padding: 8px; | ||||
|     padding-top: 10px; | ||||
|   } | ||||
| 
 | ||||
|   .status-card__title { | ||||
|     white-space: nowrap; | ||||
|   } | ||||
| 
 | ||||
|   .status-card__image { | ||||
|     flex: 0 0 60px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| a.status-card.compact:hover { | ||||
|   background-color: lighten($ui-base-color, 4%); | ||||
| } | ||||
| 
 | ||||
| .status-card__image-image { | ||||
|   border-radius: 4px 0 0 4px; | ||||
|   display: block; | ||||
|  |  | |||
|  | @ -89,6 +89,7 @@ class Status < ApplicationRecord | |||
|                    :conversation, | ||||
|                    :status_stat, | ||||
|                    :tags, | ||||
|                    :preview_cards, | ||||
|                    :stream_entry, | ||||
|                    active_mentions: :account, | ||||
|                    reblog: [ | ||||
|  | @ -96,6 +97,7 @@ class Status < ApplicationRecord | |||
|                      :application, | ||||
|                      :stream_entry, | ||||
|                      :tags, | ||||
|                      :preview_cards, | ||||
|                      :media_attachments, | ||||
|                      :conversation, | ||||
|                      :status_stat, | ||||
|  | @ -163,6 +165,10 @@ class Status < ApplicationRecord | |||
|     reblog | ||||
|   end | ||||
| 
 | ||||
|   def preview_card | ||||
|     preview_cards.first | ||||
|   end | ||||
| 
 | ||||
|   def title | ||||
|     if destroyed? | ||||
|       "#{account.acct} deleted status" | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ class REST::StatusSerializer < ActiveModel::Serializer | |||
|   has_many :tags | ||||
|   has_many :emojis, serializer: REST::CustomEmojiSerializer | ||||
| 
 | ||||
|   has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer | ||||
| 
 | ||||
|   def id | ||||
|     object.id.to_s | ||||
|   end | ||||
|  |  | |||
|  | @ -63,6 +63,7 @@ class FetchLinkCardService < BaseService | |||
| 
 | ||||
|   def attach_card | ||||
|     @status.preview_cards << @card | ||||
|     Rails.cache.delete(@status) | ||||
|   end | ||||
| 
 | ||||
|   def parse_urls | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue