Change design of link previews in web UI (#26136)
This commit is contained in:
		
					parent
					
						
							
								4d01d1a1ee
							
						
					
				
			
			
				commit
				
					
						6b2952d1dd
					
				
			
		
					 7 changed files with 90 additions and 99 deletions
				
			
		|  | @ -12,6 +12,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
| 
 | ||||
| import { Blurhash } from 'mastodon/components/blurhash'; | ||||
| import { Icon }  from 'mastodon/components/icon'; | ||||
| import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; | ||||
| import { useBlurhash } from 'mastodon/initial_state'; | ||||
| 
 | ||||
| const IDNA_PREFIX = 'xn--'; | ||||
|  | @ -57,14 +58,9 @@ export default class Card extends PureComponent { | |||
|   static propTypes = { | ||||
|     card: ImmutablePropTypes.map, | ||||
|     onOpenMedia: PropTypes.func.isRequired, | ||||
|     compact: PropTypes.bool, | ||||
|     sensitive: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     compact: false, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     previewLoaded: false, | ||||
|     embedded: false, | ||||
|  | @ -148,7 +144,7 @@ export default class Card extends PureComponent { | |||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { card, compact } = this.props; | ||||
|     const { card } = this.props; | ||||
|     const { embedded, revealed } = this.state; | ||||
| 
 | ||||
|     if (card === null) { | ||||
|  | @ -156,29 +152,24 @@ export default class Card extends PureComponent { | |||
|     } | ||||
| 
 | ||||
|     const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); | ||||
|     const horizontal  = (!compact && card.get('width') > card.get('height')) || 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 noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; | ||||
|     const language    = card.get('language') || ''; | ||||
| 
 | ||||
|     const description = ( | ||||
|       <div className='status-card__content' lang={language}> | ||||
|         {title} | ||||
|         {!(horizontal || compact) && <p className='status-card__description' title={card.get('description')}>{card.get('description')}</p>} | ||||
|         <span className='status-card__host'>{provider}</span> | ||||
|         <span className='status-card__host'>{provider}{card.get('published_at') && <> · <RelativeTimestamp timestamp={card.get('published_at')} /></>}</span> | ||||
|         <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> | ||||
|         {card.get('author_name').length > 0 && <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span>} | ||||
|       </div> | ||||
|     ); | ||||
| 
 | ||||
|     const thumbnailStyle = { | ||||
|       visibility: revealed? null : 'hidden', | ||||
|       visibility: revealed ? null : 'hidden', | ||||
|       aspectRatio: `${card.get('width')} / ${card.get('height')}` | ||||
|     }; | ||||
| 
 | ||||
|     if (horizontal) { | ||||
|       thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`; | ||||
|     } | ||||
|     let embed; | ||||
| 
 | ||||
|     let embed     = ''; | ||||
|     let canvas = ( | ||||
|       <Blurhash | ||||
|         className={classnames('status-card__image-preview', { | ||||
|  | @ -188,12 +179,15 @@ export default class Card extends PureComponent { | |||
|         dummy={!useBlurhash} | ||||
|       /> | ||||
|     ); | ||||
| 
 | ||||
|     let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />; | ||||
| 
 | ||||
|     let spoilerButton = ( | ||||
|       <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'> | ||||
|         <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> | ||||
|       </button> | ||||
|     ); | ||||
| 
 | ||||
|     spoilerButton = ( | ||||
|       <div className={classnames('spoiler-button', { 'spoiler-button--minified': revealed })}> | ||||
|         {spoilerButton} | ||||
|  | @ -219,19 +213,20 @@ export default class Card extends PureComponent { | |||
|               <div className='status-card__actions'> | ||||
|                 <div> | ||||
|                   <button type='button' onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> | ||||
|                   {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>} | ||||
|                   <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a> | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {!revealed && spoilerButton} | ||||
|           </div> | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       return ( | ||||
|         <div className={className} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}> | ||||
|         <div className='status-card' ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}> | ||||
|           {embed} | ||||
|           {!compact && description} | ||||
|           <a href={card.get('url')} target='_blank' rel='noopener noreferrer'>{description}</a> | ||||
|         </div> | ||||
|       ); | ||||
|     } else if (card.get('image')) { | ||||
|  | @ -243,14 +238,14 @@ export default class Card extends PureComponent { | |||
|       ); | ||||
|     } else { | ||||
|       embed = ( | ||||
|         <div className='status-card__image'> | ||||
|         <div className='status-card__image' style={{ aspectRatio: '1.9 / 1' }}> | ||||
|           <Icon id='file-text' /> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}> | ||||
|       <a href={card.get('url')} className='status-card' target='_blank' rel='noopener noreferrer' ref={this.setRef}> | ||||
|         {embed} | ||||
|         {description} | ||||
|       </a> | ||||
|  |  | |||
|  | @ -363,6 +363,7 @@ | |||
|   "lightbox.previous": "Previous", | ||||
|   "limited_account_hint.action": "Show profile anyway", | ||||
|   "limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.", | ||||
|   "link_preview.author": "By {name}", | ||||
|   "lists.account.add": "Add to list", | ||||
|   "lists.account.remove": "Remove from list", | ||||
|   "lists.delete": "Delete list", | ||||
|  |  | |||
|  | @ -77,6 +77,10 @@ html { | |||
|   background: $white; | ||||
| } | ||||
| 
 | ||||
| .column-header { | ||||
|   border-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .column-header__button.active { | ||||
|   color: $ui-highlight-color; | ||||
| 
 | ||||
|  | @ -414,7 +418,7 @@ html { | |||
| .column-header__collapsible-inner { | ||||
|   background: darken($ui-base-color, 4%); | ||||
|   border: 1px solid lighten($ui-base-color, 8%); | ||||
|   border-top: 0; | ||||
|   border-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .dashboard__quick-access, | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ $white: #ffffff; | |||
| $classic-base-color: #282c37; | ||||
| $classic-primary-color: #9baec8; | ||||
| $classic-secondary-color: #d9e1e8; | ||||
| $classic-highlight-color: #6364ff; | ||||
| $classic-highlight-color: #858afa; | ||||
| 
 | ||||
| $blurple-600: #563acc; // Iris | ||||
| $blurple-500: #6364ff; // Brand purple | ||||
|  |  | |||
|  | @ -252,13 +252,14 @@ | |||
| 
 | ||||
|   &.overlayed { | ||||
|     box-sizing: content-box; | ||||
|     background: rgba($base-overlay-background, 0.6); | ||||
|     color: rgba($primary-text-color, 0.7); | ||||
|     background: rgba($black, 0.65); | ||||
|     backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); | ||||
|     color: rgba($white, 0.7); | ||||
|     border-radius: 4px; | ||||
|     padding: 2px; | ||||
| 
 | ||||
|     &:hover { | ||||
|       background: rgba($base-overlay-background, 0.9); | ||||
|       background: rgba($black, 0.9); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -1352,6 +1353,10 @@ body > [data-popper-placement] { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .scrollable > div:first-child .detailed-status { | ||||
|   border-top: 0; | ||||
| } | ||||
| 
 | ||||
| .detailed-status__meta { | ||||
|   margin-top: 16px; | ||||
|   color: $dark-text-color; | ||||
|  | @ -3504,12 +3509,10 @@ button.icon-button.active i.fa-retweet { | |||
| } | ||||
| 
 | ||||
| .status-card { | ||||
|   display: block; | ||||
|   position: relative; | ||||
|   display: flex; | ||||
|   font-size: 14px; | ||||
|   border: 1px solid lighten($ui-base-color, 8%); | ||||
|   border-radius: 4px; | ||||
|   color: $dark-text-color; | ||||
|   color: $darker-text-color; | ||||
|   margin-top: 14px; | ||||
|   text-decoration: none; | ||||
|   overflow: hidden; | ||||
|  | @ -3563,8 +3566,29 @@ button.icon-button.active i.fa-retweet { | |||
| a.status-card { | ||||
|   cursor: pointer; | ||||
| 
 | ||||
|   &:hover { | ||||
|     background: lighten($ui-base-color, 8%); | ||||
|   &:hover, | ||||
|   &:focus, | ||||
|   &:active { | ||||
|     .status-card__title, | ||||
|     .status-card__host, | ||||
|     .status-card__author { | ||||
|       color: $highlight-text-color; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .status-card a { | ||||
|   color: inherit; | ||||
|   text-decoration: none; | ||||
| 
 | ||||
|   &:hover, | ||||
|   &:focus, | ||||
|   &:active { | ||||
|     .status-card__title, | ||||
|     .status-card__host, | ||||
|     .status-card__author { | ||||
|       color: $highlight-text-color; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -3590,42 +3614,42 @@ a.status-card { | |||
| 
 | ||||
| .status-card__title { | ||||
|   display: block; | ||||
|   font-weight: 500; | ||||
|   margin-bottom: 5px; | ||||
|   color: $darker-text-color; | ||||
|   font-weight: 700; | ||||
|   font-size: 19px; | ||||
|   line-height: 24px; | ||||
|   color: $primary-text-color; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .status-card__content { | ||||
|   flex: 1 1 auto; | ||||
|   overflow: hidden; | ||||
|   padding: 14px 14px 14px 8px; | ||||
| } | ||||
| 
 | ||||
| .status-card__description { | ||||
|   color: $darker-text-color; | ||||
|   overflow: hidden; | ||||
|   display: -webkit-box; | ||||
|   -webkit-box-orient: vertical; | ||||
|   -webkit-line-clamp: 2; | ||||
|   padding: 15px 0; | ||||
|   padding-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .status-card__host { | ||||
|   display: block; | ||||
|   margin-top: 5px; | ||||
|   font-size: 13px; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   font-size: 14px; | ||||
|   margin-bottom: 8px; | ||||
| } | ||||
| 
 | ||||
| .status-card__author { | ||||
|   display: block; | ||||
|   margin-top: 8px; | ||||
|   font-size: 14px; | ||||
|   color: $primary-text-color; | ||||
| 
 | ||||
|   strong { | ||||
|     font-weight: 500; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .status-card__image { | ||||
|   flex: 0 0 100px; | ||||
|   width: 100%; | ||||
|   background: lighten($ui-base-color, 8%); | ||||
|   position: relative; | ||||
|   border-radius: 8px; | ||||
| 
 | ||||
|   & > .fa { | ||||
|     font-size: 21px; | ||||
|  | @ -3637,50 +3661,8 @@ a.status-card { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .status-card.horizontal { | ||||
|   display: block; | ||||
| 
 | ||||
|   .status-card__image { | ||||
|     width: 100%; | ||||
|   } | ||||
| 
 | ||||
|   .status-card__image-image, | ||||
|   .status-card__image-preview { | ||||
|     border-radius: 4px 4px 0 0; | ||||
|   } | ||||
| 
 | ||||
|   .status-card__title { | ||||
|     white-space: inherit; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   border-radius: 8px; | ||||
|   display: block; | ||||
|   margin: 0; | ||||
|   width: 100%; | ||||
|  | @ -3691,7 +3673,7 @@ a.status-card.compact:hover { | |||
| } | ||||
| 
 | ||||
| .status-card__image-preview { | ||||
|   border-radius: 4px 0 0 4px; | ||||
|   border-radius: 8px; | ||||
|   display: block; | ||||
|   margin: 0; | ||||
|   width: 100%; | ||||
|  |  | |||
|  | @ -124,6 +124,7 @@ class LinkDetailsExtractor | |||
|       author_url: author_url || '', | ||||
|       embed_url: embed_url || '', | ||||
|       language: language, | ||||
|       created_at: published_at, | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|  | @ -159,6 +160,10 @@ class LinkDetailsExtractor | |||
|     html_entities.decode(structured_data&.description || opengraph_tag('og:description') || meta_tag('description')) | ||||
|   end | ||||
| 
 | ||||
|   def published_at | ||||
|     structured_data&.date_published || opengraph_tag('article:published_time') | ||||
|   end | ||||
| 
 | ||||
|   def image | ||||
|     valid_url_or_nil(opengraph_tag('og:image')) | ||||
|   end | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer | |||
|   attributes :url, :title, :description, :language, :type, | ||||
|              :author_name, :author_url, :provider_name, | ||||
|              :provider_url, :html, :width, :height, | ||||
|              :image, :embed_url, :blurhash | ||||
|              :image, :embed_url, :blurhash, :published_at | ||||
| 
 | ||||
|   def image | ||||
|     object.image? ? full_asset_url(object.image.url(:original)) : nil | ||||
|  | @ -15,4 +15,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer | |||
|   def html | ||||
|     Sanitize.fragment(object.html, Sanitize::Config::MASTODON_OEMBED) | ||||
|   end | ||||
| 
 | ||||
|   def published_at | ||||
|     object.created_at | ||||
|   end | ||||
| end | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue