When avatar/header are GIF, generate static versions (#1428)
* When avatar/header are GIF, generate static versions. Account API returns "avatar"/"avatar_static", "header"/"header_static" Static version is the same as original for other cases Web UI de-animates avatars in toots, lists of users Fix #441, fix #596, prerequisite for #1064 * Fix JS test * Add rake task to generate static avatars/headers from GIF ones, add test
This commit is contained in:
		
							parent
							
								
									b57eed4584
								
							
						
					
					
						commit
						12f72e1740
					
				|  | @ -65,7 +65,7 @@ const Account = React.createClass({ | ||||||
|       <div className='account'> |       <div className='account'> | ||||||
|         <div style={{ display: 'flex' }}> |         <div style={{ display: 'flex' }}> | ||||||
|           <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> |           <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> | ||||||
|             <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div> |             <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={36} /></div> | ||||||
|             <DisplayName account={account} /> |             <DisplayName account={account} /> | ||||||
|           </Permalink> |           </Permalink> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,103 +1,18 @@ | ||||||
| import PureRenderMixin from 'react-addons-pure-render-mixin'; | import PureRenderMixin from 'react-addons-pure-render-mixin'; | ||||||
| 
 | 
 | ||||||
| // From: http://stackoverflow.com/a/18320662 |  | ||||||
| const resample = (canvas, width, height, resize_canvas) => { |  | ||||||
|   let width_source  = canvas.width; |  | ||||||
|   let height_source = canvas.height; |  | ||||||
|   width  = Math.round(width); |  | ||||||
|   height = Math.round(height); |  | ||||||
| 
 |  | ||||||
|   let ratio_w      = width_source / width; |  | ||||||
|   let ratio_h      = height_source / height; |  | ||||||
|   let ratio_w_half = Math.ceil(ratio_w / 2); |  | ||||||
|   let ratio_h_half = Math.ceil(ratio_h / 2); |  | ||||||
| 
 |  | ||||||
|   let ctx   = canvas.getContext("2d"); |  | ||||||
|   let img   = ctx.getImageData(0, 0, width_source, height_source); |  | ||||||
|   let img2  = ctx.createImageData(width, height); |  | ||||||
|   let data  = img.data; |  | ||||||
|   let data2 = img2.data; |  | ||||||
| 
 |  | ||||||
|   for (let j = 0; j < height; j++) { |  | ||||||
|     for (let i = 0; i < width; i++) { |  | ||||||
|       let x2            = (i + j * width) * 4; |  | ||||||
|       let weight        = 0; |  | ||||||
|       let weights       = 0; |  | ||||||
|       let weights_alpha = 0; |  | ||||||
|       let gx_r          = 0; |  | ||||||
|       let gx_g          = 0; |  | ||||||
|       let gx_b          = 0; |  | ||||||
|       let gx_a          = 0; |  | ||||||
|       let center_y      = (j + 0.5) * ratio_h; |  | ||||||
|       let yy_start      = Math.floor(j * ratio_h); |  | ||||||
|       let yy_stop       = Math.ceil((j + 1) * ratio_h); |  | ||||||
| 
 |  | ||||||
|       for (let yy = yy_start; yy < yy_stop; yy++) { |  | ||||||
|         let dy       = Math.abs(center_y - (yy + 0.5)) / ratio_h_half; |  | ||||||
|         let center_x = (i + 0.5) * ratio_w; |  | ||||||
|         let w0       = dy * dy; //pre-calc part of w |  | ||||||
|         let xx_start = Math.floor(i * ratio_w); |  | ||||||
|         let xx_stop  = Math.ceil((i + 1) * ratio_w); |  | ||||||
| 
 |  | ||||||
|         for (let xx = xx_start; xx < xx_stop; xx++) { |  | ||||||
|           let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half; |  | ||||||
|           let w  = Math.sqrt(w0 + dx * dx); |  | ||||||
| 
 |  | ||||||
|           if (w >= 1) { |  | ||||||
|             // pixel too far |  | ||||||
|             continue; |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           // hermite filter |  | ||||||
|           weight    = 2 * w * w * w - 3 * w * w + 1; |  | ||||||
|           let pos_x = 4 * (xx + yy * width_source); |  | ||||||
| 
 |  | ||||||
|           // alpha |  | ||||||
|           gx_a          += weight * data[pos_x + 3]; |  | ||||||
|           weights_alpha += weight; |  | ||||||
| 
 |  | ||||||
|           // colors |  | ||||||
|           if (data[pos_x + 3] < 255) |  | ||||||
|             weight = weight * data[pos_x + 3] / 250; |  | ||||||
| 
 |  | ||||||
|           gx_r    += weight * data[pos_x]; |  | ||||||
|           gx_g    += weight * data[pos_x + 1]; |  | ||||||
|           gx_b    += weight * data[pos_x + 2]; |  | ||||||
|           weights += weight; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       data2[x2]     = gx_r / weights; |  | ||||||
|       data2[x2 + 1] = gx_g / weights; |  | ||||||
|       data2[x2 + 2] = gx_b / weights; |  | ||||||
|       data2[x2 + 3] = gx_a / weights_alpha; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // clear and resize canvas |  | ||||||
|   if (resize_canvas === true) { |  | ||||||
|     canvas.width  = width; |  | ||||||
|     canvas.height = height; |  | ||||||
|   } else { |  | ||||||
|     ctx.clearRect(0, 0, width_source, height_source); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // draw |  | ||||||
|   ctx.putImageData(img2, 0, 0); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const Avatar = React.createClass({ | const Avatar = React.createClass({ | ||||||
| 
 | 
 | ||||||
|   propTypes: { |   propTypes: { | ||||||
|     src: React.PropTypes.string.isRequired, |     src: React.PropTypes.string.isRequired, | ||||||
|  |     staticSrc: React.PropTypes.string, | ||||||
|     size: React.PropTypes.number.isRequired, |     size: React.PropTypes.number.isRequired, | ||||||
|     style: React.PropTypes.object, |     style: React.PropTypes.object, | ||||||
|     animated: React.PropTypes.bool |     animate: React.PropTypes.bool | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   getDefaultProps () { |   getDefaultProps () { | ||||||
|     return { |     return { | ||||||
|       animated: true |       animate: false | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|  | @ -117,38 +32,30 @@ const Avatar = React.createClass({ | ||||||
|     this.setState({ hovering: false }); |     this.setState({ hovering: false }); | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   handleLoad () { |  | ||||||
|     this.canvas.width  = this.image.naturalWidth; |  | ||||||
|     this.canvas.height = this.image.naturalHeight; |  | ||||||
|     this.canvas.getContext('2d').drawImage(this.image, 0, 0); |  | ||||||
| 
 |  | ||||||
|     resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true); |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   setImageRef (c) { |  | ||||||
|     this.image = c; |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   setCanvasRef (c) { |  | ||||||
|     this.canvas = c; |  | ||||||
|   }, |  | ||||||
| 
 |  | ||||||
|   render () { |   render () { | ||||||
|  |     const { src, size, staticSrc, animate } = this.props; | ||||||
|     const { hovering } = this.state; |     const { hovering } = this.state; | ||||||
| 
 | 
 | ||||||
|     if (this.props.animated) { |     const style = { | ||||||
|       return ( |       ...this.props.style, | ||||||
|         <div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}> |       width: `${size}px`, | ||||||
|           <img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} /> |       height: `${size}px`, | ||||||
|         </div> |       backgroundSize: `${size}px ${size}px` | ||||||
|       ); |     }; | ||||||
|  | 
 | ||||||
|  |     if (hovering || animate) { | ||||||
|  |       style.backgroundImage = `url(${src})`; | ||||||
|  |     } else { | ||||||
|  |       style.backgroundImage = `url(${staticSrc})`; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}> |       <div | ||||||
|         <img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} /> |         className='avatar' | ||||||
|         <canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} /> |         onMouseEnter={this.handleMouseEnter} | ||||||
|       </div> |         onMouseLeave={this.handleMouseLeave} | ||||||
|  |         style={style} | ||||||
|  |       /> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -90,7 +90,7 @@ const Status = React.createClass({ | ||||||
| 
 | 
 | ||||||
|           <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}> |           <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}> | ||||||
|             <div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}> |             <div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}> | ||||||
|               <Avatar src={status.getIn(['account', 'avatar'])} size={48} /> |               <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|             <DisplayName account={status.get('account')} /> |             <DisplayName account={status.get('account')} /> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
| 
 | 
 | ||||||
| const AutosuggestAccount = ({ account }) => ( | const AutosuggestAccount = ({ account }) => ( | ||||||
|   <div style={{ overflow: 'hidden' }} className='autosuggest-account'> |   <div style={{ overflow: 'hidden' }} className='autosuggest-account'> | ||||||
|     <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div> |     <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={18} /></div> | ||||||
|     <DisplayName account={account} /> |     <DisplayName account={account} /> | ||||||
|   </div> |   </div> | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ const NavigationBar = React.createClass({ | ||||||
|   render () { |   render () { | ||||||
|     return ( |     return ( | ||||||
|       <div className='navigation-bar'> |       <div className='navigation-bar'> | ||||||
|         <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink> |         <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink> | ||||||
| 
 | 
 | ||||||
|         <div style={{ flex: '1 1 auto', marginLeft: '8px' }}> |         <div style={{ flex: '1 1 auto', marginLeft: '8px' }}> | ||||||
|           <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong> |           <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong> | ||||||
|  |  | ||||||
|  | @ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({ | ||||||
|           <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> |           <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> | ||||||
| 
 | 
 | ||||||
|           <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}> |           <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}> | ||||||
|             <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div> |             <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div> | ||||||
|             <DisplayName account={status.get('account')} /> |             <DisplayName account={status.get('account')} /> | ||||||
|           </a> |           </a> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|  | @ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => { | ||||||
|     <div> |     <div> | ||||||
|       <div style={outerStyle}> |       <div style={outerStyle}> | ||||||
|         <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> |         <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> | ||||||
|           <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div> |           <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> | ||||||
|           <DisplayName account={account} /> |           <DisplayName account={account} /> | ||||||
|         </Permalink> |         </Permalink> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ const DetailedStatus = React.createClass({ | ||||||
|     return ( |     return ( | ||||||
|       <div style={{ padding: '14px 10px' }} className='detailed-status'> |       <div style={{ padding: '14px 10px' }} className='detailed-status'> | ||||||
|         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> |         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> | ||||||
|           <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div> |           <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> | ||||||
|           <DisplayName account={status.get('account')} /> |           <DisplayName account={status.get('account')} /> | ||||||
|         </a> |         </a> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -165,6 +165,14 @@ | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .avatar { | ||||||
|  |   border-radius: 4px; | ||||||
|  |   background: transparent no-repeat; | ||||||
|  |   background-position: 50%; | ||||||
|  |   background-clip: padding-box; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .lightbox .icon-button { | .lightbox .icon-button { | ||||||
|   color: $color1; |   color: $color1; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -12,12 +12,12 @@ class Account < ApplicationRecord | ||||||
|   validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?' |   validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?' | ||||||
| 
 | 
 | ||||||
|   # Avatar upload |   # Avatar upload | ||||||
|   has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } |   has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' } | ||||||
|   validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES |   validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES | ||||||
|   validates_attachment_size :avatar, less_than: 2.megabytes |   validates_attachment_size :avatar, less_than: 2.megabytes | ||||||
| 
 | 
 | ||||||
|   # Header upload |   # Header upload | ||||||
|   has_attached_file :header, styles: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' } |   has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' } | ||||||
|   validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES |   validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES | ||||||
|   validates_attachment_size :header, less_than: 2.megabytes |   validates_attachment_size :header, less_than: 2.megabytes | ||||||
| 
 | 
 | ||||||
|  | @ -158,6 +158,22 @@ class Account < ApplicationRecord | ||||||
|     save! |     save! | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def avatar_original_url | ||||||
|  |     avatar.url(:original) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def avatar_static_url | ||||||
|  |     avatar_content_type == 'image/gif' ? avatar.url(:static) : avatar_original_url | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def header_original_url | ||||||
|  |     header.url(:original) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def header_static_url | ||||||
|  |     header_content_type == 'image/gif' ? header.url(:static) : header_original_url | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def avatar_remote_url=(url) |   def avatar_remote_url=(url) | ||||||
|     parsed_url = URI.parse(url) |     parsed_url = URI.parse(url) | ||||||
| 
 | 
 | ||||||
|  | @ -292,6 +308,18 @@ class Account < ApplicationRecord | ||||||
|     def follow_mapping(query, field) |     def follow_mapping(query, field) | ||||||
|       query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping } |       query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping } | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     def avatar_styles(file) | ||||||
|  |       styles = { original: '120x120#' } | ||||||
|  |       styles[:static] = { format: 'png' } if file.content_type == 'image/gif' | ||||||
|  |       styles | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def header_styles(file) | ||||||
|  |       styles = { original: '700x335#' } | ||||||
|  |       styles[:static] = { format: 'png' } if file.content_type == 'image/gif' | ||||||
|  |       styles | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   before_create do |   before_create do | ||||||
|  |  | ||||||
|  | @ -4,8 +4,9 @@ attributes :id, :username, :acct, :display_name, :locked, :created_at | ||||||
| 
 | 
 | ||||||
| node(:note)            { |account| Formatter.instance.simplified_format(account) } | node(:note)            { |account| Formatter.instance.simplified_format(account) } | ||||||
| node(:url)             { |account| TagManager.instance.url_for(account) } | node(:url)             { |account| TagManager.instance.url_for(account) } | ||||||
| node(:avatar)          { |account| full_asset_url(account.avatar.url(:original)) } | node(:avatar)          { |account| full_asset_url(account.avatar_original_url) } | ||||||
| node(:header)          { |account| full_asset_url(account.header.url(:original)) } | node(:avatar_static)   { |account| full_asset_url(account.avatar_static_url) } | ||||||
| node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count } | node(:header)          { |account| full_asset_url(account.header_original_url) } | ||||||
| node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count } | node(:header_static)   { |account| full_asset_url(account.header_static_url) } | ||||||
| node(:statuses_count)  { |account| defined?(@statuses_counts_map)  ? (@statuses_counts_map[account.id]  || 0) : account.statuses_count } | 
 | ||||||
|  | attributes :followers_count, :following_count, :statuses_count | ||||||
|  |  | ||||||
|  | @ -92,5 +92,17 @@ namespace :mastodon do | ||||||
| 
 | 
 | ||||||
|       Rails.logger.debug 'Done!' |       Rails.logger.debug 'Done!' | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     desc 'Generate static versions of GIF avatars/headers' | ||||||
|  |     task add_static_avatars: :environment do | ||||||
|  |       Rails.logger.debug 'Generating static avatars/headers for GIF ones...' | ||||||
|  | 
 | ||||||
|  |       Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account| | ||||||
|  |         account.avatar.reprocess! | ||||||
|  |         account.header.reprocess! | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       Rails.logger.debug 'Done!' | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 84 KiB | 
|  | @ -6,16 +6,10 @@ import Avatar from '../../../app/assets/javascripts/components/components/avatar | ||||||
| describe('<Avatar />', () => { | describe('<Avatar />', () => { | ||||||
|   const src = '/path/to/image.jpg'; |   const src = '/path/to/image.jpg'; | ||||||
|   const size = 100; |   const size = 100; | ||||||
|   const wrapper = render(<Avatar src={src} size={size} />); |   const wrapper = render(<Avatar src={src} animate size={size} />); | ||||||
| 
 | 
 | ||||||
|   it('renders an img element with the given src', () => { |   it('renders a div element with the given src as background', () => { | ||||||
|     expect(wrapper.find('img')).to.have.attr('src', `${src}`); |     expect(wrapper.find('div')).to.have.style('background-image', `url(${src})`); | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('renders an img element of the given size', () => { |  | ||||||
|     ['width', 'height'].map((attr) => { |  | ||||||
|       expect(wrapper.find('img')).to.have.attr(attr, `${size}`); |  | ||||||
|     }); |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('renders a div element of the given size', () => { |   it('renders a div element of the given size', () => { | ||||||
|  |  | ||||||
|  | @ -421,4 +421,24 @@ RSpec.describe Account, type: :model do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe 'static avatars' do | ||||||
|  |     describe 'when GIF' do | ||||||
|  |       it 'creates a png static style' do | ||||||
|  |         subject.avatar = attachment_fixture('avatar.gif') | ||||||
|  |         subject.save | ||||||
|  | 
 | ||||||
|  |         expect(subject.avatar_static_url).to_not eq subject.avatar_original_url | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     describe 'when non-GIF' do | ||||||
|  |       it 'does not create extra static style' do | ||||||
|  |         subject.avatar = attachment_fixture('attachment.jpg') | ||||||
|  |         subject.save | ||||||
|  | 
 | ||||||
|  |         expect(subject.avatar_static_url).to eq subject.avatar_original_url | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue