sforkowany z mirror/soapbox
				
			Placeholder: display placeholder statuses while timelines are loading
							rodzic
							
								
									d41e3f96ee
								
							
						
					
					
						commit
						47b433915b
					
				|  | @ -29,6 +29,8 @@ export default class ScrollableList extends PureComponent { | |||
|     children: PropTypes.node, | ||||
|     onScrollToTop: PropTypes.func, | ||||
|     onScroll: PropTypes.func, | ||||
|     placeholderComponent: PropTypes.node, | ||||
|     placeholderCount: PropTypes.number, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|  | @ -222,7 +224,13 @@ export default class ScrollableList extends PureComponent { | |||
|   } | ||||
| 
 | ||||
|   renderLoading = () => { | ||||
|     const { prepend } = this.props; | ||||
|     const { prepend, placeholderComponent: Placeholder, placeholderCount } = this.props; | ||||
| 
 | ||||
|     if (Placeholder && placeholderCount > 0) { | ||||
|       return Array(placeholderCount).fill().map((_, i) => ( | ||||
|         <Placeholder key={i} /> | ||||
|       )); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='slist slist--flex'> | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
| import LoadGap from './load_gap'; | ||||
| import ScrollableList from './scrollable_list'; | ||||
| import TimelineQueueButtonHeader from './timeline_queue_button_header'; | ||||
| import PlaceholderMaterialStatus from 'soapbox/features/placeholder/components/placeholder_material_status'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' }, | ||||
|  | @ -213,7 +214,16 @@ export default class StatusList extends ImmutablePureComponent { | |||
|         count={totalQueuedItemsCount} | ||||
|         message={messages.queue} | ||||
|       />, | ||||
|       <ScrollableList key='scrollable-list' {...other} isLoading={isLoading} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}> | ||||
|       <ScrollableList | ||||
|         key='scrollable-list' | ||||
|         isLoading={isLoading} | ||||
|         showLoading={isLoading && statusIds.size === 0} | ||||
|         onLoadMore={onLoadMore && this.handleLoadOlder} | ||||
|         placeholderComponent={PlaceholderMaterialStatus} | ||||
|         placeholderCount={20} | ||||
|         ref={this.setRef} | ||||
|         {...other} | ||||
|       > | ||||
|         {this.renderScrollableContent()} | ||||
|       </ScrollableList>, | ||||
|     ]; | ||||
|  |  | |||
|  | @ -0,0 +1,35 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| export default class Avatar extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     size: PropTypes.number, | ||||
|     style: PropTypes.object, | ||||
|     inline: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     inline: false, | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { size, inline } = this.props; | ||||
| 
 | ||||
|     // : TODO : remove inline and change all avatars to be sized using css
 | ||||
|     const style = !size ? {} : { | ||||
|       width: `${size}px`, | ||||
|       height: `${size}px`, | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|       <div | ||||
|         className={classNames('account__avatar', { 'account__avatar-inline': inline })} | ||||
|         style={style} | ||||
|         alt='' | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,27 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { randomIntFromInterval, generateText } from '../utils'; | ||||
| 
 | ||||
| export default class DisplayName extends React.Component { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     maxLength: PropTypes.number.isRequired, | ||||
|     minLength: PropTypes.number.isRequired, | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     const { maxLength, minLength } = this.props; | ||||
|     const length = randomIntFromInterval(maxLength, minLength); | ||||
| 
 | ||||
|     return ( | ||||
|       <span className='display-name'> | ||||
|         <span> | ||||
|           <span className='display-name__name'> | ||||
|             <bdi><strong className='display-name__html'>{generateText(length)}</strong></bdi> | ||||
|           </span> | ||||
|         </span> | ||||
|       </span> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| import React from 'react'; | ||||
| import PlaceholderStatus from './placeholder_status'; | ||||
| 
 | ||||
| export default class PlaceholderMaterialStatus extends React.Component { | ||||
| 
 | ||||
|   render() { | ||||
|     return ( | ||||
|       <div className='material-status' tabIndex={-1} aria-hidden> | ||||
|         <div className='material-status__status' tabIndex={0}> | ||||
|           <PlaceholderStatus {...this.props} focusable={false} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,40 @@ | |||
| import React from 'react'; | ||||
| import PlaceholderAvatar from './placeholder_avatar'; | ||||
| import PlaceholderDisplayName from './placeholder_display_name'; | ||||
| import PlaceholderStatusContent from './placeholder_status_content'; | ||||
| 
 | ||||
| export default class PlaceholderStatus extends React.Component { | ||||
| 
 | ||||
|   shouldComponentUpdate() { | ||||
|     // Re-rendering this will just cause the random lengths to jump around.
 | ||||
|     // There's basically no reason to ever do it.
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     return ( | ||||
|       <div className='placeholder-status'> | ||||
|         <div className='status__wrapper'> | ||||
|           <div className='status'> | ||||
|             <div className='status__info'> | ||||
|               <div className='status__profile'> | ||||
|                 <div className='status__avatar'> | ||||
|                   <PlaceholderAvatar size={48} /> | ||||
|                 </div> | ||||
|                 <span className='status__display-name'> | ||||
|                   <PlaceholderDisplayName minLength={3} maxLength={25} /> | ||||
|                 </span> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <PlaceholderStatusContent minLength={5} maxLength={120} /> | ||||
| 
 | ||||
|             {/* TODO */} | ||||
|             {/* <PlaceholderActionBar /> */} | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,23 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { randomIntFromInterval, generateText } from '../utils'; | ||||
| 
 | ||||
| export default class PlaceholderStatusContent extends React.Component { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     maxLength: PropTypes.number.isRequired, | ||||
|     minLength: PropTypes.number.isRequired, | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     const { maxLength, minLength } = this.props; | ||||
|     const length = randomIntFromInterval(maxLength, minLength); | ||||
| 
 | ||||
|     return( | ||||
|       <div className='status__content' tabIndex='0' key='content'> | ||||
|         {generateText(length)} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| export const PLACEHOLDER_CHAR = '█'; | ||||
| 
 | ||||
| export const generateText = length => { | ||||
|   let text = ''; | ||||
| 
 | ||||
|   for (let i = 0; i < length; i++) { | ||||
|     text += PLACEHOLDER_CHAR; | ||||
|   } | ||||
| 
 | ||||
|   return text; | ||||
| }; | ||||
| 
 | ||||
| // https://stackoverflow.com/a/7228322/8811886
 | ||||
| export const randomIntFromInterval = (min, max) => { | ||||
|   return Math.floor(Math.random() * (max - min + 1) + min); | ||||
| }; | ||||
|  | @ -204,3 +204,12 @@ noscript { | |||
| .greentext { | ||||
|   color: #789922; | ||||
| } | ||||
| 
 | ||||
| .placeholder-status { | ||||
|   .status__display-name strong, | ||||
|   .status__content { | ||||
|     letter-spacing: -1px; | ||||
|     color: var(--brand-color); | ||||
|     opacity: 0.1; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -881,7 +881,8 @@ | |||
| } | ||||
| 
 | ||||
| // Make MaterialStatus flush against SubNavigation | ||||
| .sub-navigation ~ .slist .item-list > article:first-child .material-status__status { | ||||
| .sub-navigation ~ .slist .item-list > article:first-child .material-status__status, | ||||
| .sub-navigation ~ .material-status .material-status__status { | ||||
|   border-top-left-radius: 0; | ||||
|   border-top-right-radius: 0; | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| .display-name__account { | ||||
|   position: relative; | ||||
|   cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .display-name .profile-hover-card { | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Alex Gleason
						Alex Gleason