parent
							
								
									ecdac9017e
								
							
						
					
					
						commit
						f7765acf9d
					
				
					 3 changed files with 150 additions and 79 deletions
				
			
		|  | @ -20,6 +20,16 @@ const getHostname = url => { | |||
|   return parser.hostname; | ||||
| }; | ||||
| 
 | ||||
| const trim = (text, len) => { | ||||
|   const cut = text.indexOf(' ', len); | ||||
| 
 | ||||
|   if (cut === -1) { | ||||
|     return text; | ||||
|   } | ||||
| 
 | ||||
|   return text.substring(0, cut) + (text.length > len ? '…' : ''); | ||||
| }; | ||||
| 
 | ||||
| export default class Card extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|  | @ -33,9 +43,16 @@ export default class Card extends React.PureComponent { | |||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     width: 0, | ||||
|     width: 280, | ||||
|     embedded: false, | ||||
|   }; | ||||
| 
 | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     if (this.props.card !== nextProps.card) { | ||||
|       this.setState({ embedded: false }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handlePhotoClick = () => { | ||||
|     const { card, onOpenMedia } = this.props; | ||||
| 
 | ||||
|  | @ -57,56 +74,14 @@ export default class Card extends React.PureComponent { | |||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   renderLink () { | ||||
|     const { card, maxDescription } = this.props; | ||||
|     const { width }  = this.state; | ||||
|     const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width); | ||||
| 
 | ||||
|     let image    = ''; | ||||
|     let provider = card.get('provider_name'); | ||||
| 
 | ||||
|     if (card.get('image')) { | ||||
|       image = ( | ||||
|         <div className='status-card__image'> | ||||
|           <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} /> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (provider.length < 1) { | ||||
|       provider = decodeIDNA(getHostname(card.get('url'))); | ||||
|     } | ||||
| 
 | ||||
|     const className = classnames('status-card', { horizontal }); | ||||
| 
 | ||||
|     return ( | ||||
|       <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> | ||||
|         {image} | ||||
| 
 | ||||
|         <div className='status-card__content'> | ||||
|           <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> | ||||
|           {!horizontal && <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>} | ||||
|           <span className='status-card__host'>{provider}</span> | ||||
|         </div> | ||||
|       </a> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   renderPhoto () { | ||||
|   handleEmbedClick = () => { | ||||
|     const { card } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <img | ||||
|         className='status-card-photo' | ||||
|         onClick={this.handlePhotoClick} | ||||
|         role='button' | ||||
|         tabIndex='0' | ||||
|         src={card.get('embed_url')} | ||||
|         alt={card.get('title')} | ||||
|         width={card.get('width')} | ||||
|         height={card.get('height')} | ||||
|       /> | ||||
|     ); | ||||
|     if (card.get('type') === 'photo') { | ||||
|       this.handlePhotoClick(); | ||||
|     } else { | ||||
|       this.setState({ embedded: true }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|  | @ -125,7 +100,7 @@ export default class Card extends React.PureComponent { | |||
|     return ( | ||||
|       <div | ||||
|         ref={this.setRef} | ||||
|         className='status-card-video' | ||||
|         className='status-card__image status-card-video' | ||||
|         dangerouslySetInnerHTML={content} | ||||
|         style={{ height }} | ||||
|       /> | ||||
|  | @ -133,23 +108,76 @@ export default class Card extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { card } = this.props; | ||||
|     const { card, maxDescription } = this.props; | ||||
|     const { width, embedded }      = this.state; | ||||
| 
 | ||||
|     if (card === null) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     switch(card.get('type')) { | ||||
|     case 'link': | ||||
|       return this.renderLink(); | ||||
|     case 'photo': | ||||
|       return this.renderPhoto(); | ||||
|     case 'video': | ||||
|       return this.renderVideo(); | ||||
|     case 'rich': | ||||
|     default: | ||||
|       return null; | ||||
|     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 interactive = card.get('type') !== 'link'; | ||||
|     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 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>} | ||||
|         <span className='status-card__host'>{provider}</span> | ||||
|       </div> | ||||
|     ); | ||||
| 
 | ||||
|     let embed     = ''; | ||||
|     let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />; | ||||
| 
 | ||||
|     if (interactive) { | ||||
|       if (embedded) { | ||||
|         embed = this.renderVideo(); | ||||
|       } else { | ||||
|         let iconVariant = 'play'; | ||||
| 
 | ||||
|         if (card.get('type') === 'photo') { | ||||
|           iconVariant = 'search-plus'; | ||||
|         } | ||||
| 
 | ||||
|         embed = ( | ||||
|           <div className='status-card__image'> | ||||
|             {thumbnail} | ||||
| 
 | ||||
|             <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> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       return ( | ||||
|         <div className={className} ref={this.setRef}> | ||||
|           {embed} | ||||
|           {description} | ||||
|         </div> | ||||
|       ); | ||||
|     } else if (card.get('image')) { | ||||
|       embed = ( | ||||
|         <div className='status-card__image'> | ||||
|           {thumbnail} | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> | ||||
|         {embed} | ||||
|         {description} | ||||
|       </a> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -2208,7 +2208,6 @@ | |||
| 
 | ||||
| .status-card { | ||||
|   display: flex; | ||||
|   cursor: pointer; | ||||
|   font-size: 14px; | ||||
|   border: 1px solid lighten($ui-base-color, 8%); | ||||
|   border-radius: 4px; | ||||
|  | @ -2217,20 +2216,58 @@ | |||
|   text-decoration: none; | ||||
|   overflow: hidden; | ||||
| 
 | ||||
|   &:hover { | ||||
|     background: lighten($ui-base-color, 8%); | ||||
|   &__actions { | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     position: absolute; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
| 
 | ||||
|     & > div { | ||||
|       background: rgba($base-shadow-color, 0.6); | ||||
|       border-radius: 4px; | ||||
|       padding: 12px 9px; | ||||
|       flex: 0 0 auto; | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     button, | ||||
|     a { | ||||
|       display: inline; | ||||
|       color: $primary-text-color; | ||||
|       background: transparent; | ||||
|       border: 0; | ||||
|       padding: 0 5px; | ||||
|       text-decoration: none; | ||||
|       opacity: 0.6; | ||||
|       font-size: 18px; | ||||
|       line-height: 18px; | ||||
| 
 | ||||
|       &:hover, | ||||
|       &:active, | ||||
|       &:focus { | ||||
|         opacity: 1; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     a { | ||||
|       font-size: 19px; | ||||
|       position: relative; | ||||
|       bottom: -1px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .status-card-video, | ||||
| .status-card-rich, | ||||
| .status-card-photo { | ||||
|   margin-top: 14px; | ||||
|   overflow: hidden; | ||||
| a.status-card { | ||||
|   cursor: pointer; | ||||
| 
 | ||||
|   iframe { | ||||
|     width: 100%; | ||||
|     height: auto; | ||||
|   &:hover { | ||||
|     background: lighten($ui-base-color, 8%); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -2258,6 +2295,7 @@ | |||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .status-card__content { | ||||
|  | @ -2279,6 +2317,7 @@ | |||
| .status-card__image { | ||||
|   flex: 0 0 100px; | ||||
|   background: lighten($ui-base-color, 8%); | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .status-card.horizontal { | ||||
|  | @ -2304,6 +2343,8 @@ | |||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   object-fit: cover; | ||||
|   background-size: cover; | ||||
|   background-position: center center; | ||||
| } | ||||
| 
 | ||||
| .load-more { | ||||
|  |  | |||
|  | @ -94,14 +94,16 @@ class FetchLinkCardService < BaseService | |||
|       @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url) | ||||
|     when 'photo' | ||||
|       return false unless embed.respond_to?(:url) | ||||
| 
 | ||||
|       @card.embed_url        = embed.url | ||||
|       @card.image_remote_url = embed.url | ||||
|       @card.width            = embed.width.presence  || 0 | ||||
|       @card.height           = embed.height.presence || 0 | ||||
|     when 'video' | ||||
|       @card.width  = embed.width.presence  || 0 | ||||
|       @card.height = embed.height.presence || 0 | ||||
|       @card.html   = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED) | ||||
|       @card.width            = embed.width.presence  || 0 | ||||
|       @card.height           = embed.height.presence || 0 | ||||
|       @card.html             = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED) | ||||
|       @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url) | ||||
|     when 'rich' | ||||
|       # Most providers rely on <script> tags, which is a no-no | ||||
|       return false | ||||
|  | @ -130,12 +132,12 @@ class FetchLinkCardService < BaseService | |||
|                                                scrolling: 'no', | ||||
|                                                frameborder: '0') | ||||
|     else | ||||
|       @card.type             = :link | ||||
|       @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image') | ||||
|       @card.type = :link | ||||
|     end | ||||
| 
 | ||||
|     @card.title       = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || '' | ||||
|     @card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || '' | ||||
|     @card.title            = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || '' | ||||
|     @card.description      = meta_property(page, 'og:description').presence || meta_property(page, 'description') || '' | ||||
|     @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image') | ||||
| 
 | ||||
|     return if @card.title.blank? && @card.html.blank? | ||||
| 
 | ||||
|  |  | |||
		Reference in a new issue