* Always use the scrollable class for the ScrollList component Fixes #9499 as well as empty timelines' scrollbar “blinking” between loading state and empty state. * Do not display empty message when the list is known to have more elements Fixes #9500 * Fix LoadMore usage in account media gallery (Even though that codepath is probably never actually used…) * Make account media gallery more consistent with account timeline Fixes #9498 Display “load more” more consistently, add a loading indicator on first load. * Fix “load more” button when no data has been fetched
		
			
				
	
	
		
			133 lines
		
	
	
	
		
			4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			133 lines
		
	
	
	
		
			4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { debounce } from 'lodash';
 | |
| import React from 'react';
 | |
| import { FormattedMessage } from 'react-intl';
 | |
| import ImmutablePropTypes from 'react-immutable-proptypes';
 | |
| import PropTypes from 'prop-types';
 | |
| import StatusContainer from '../containers/status_container';
 | |
| import ImmutablePureComponent from 'react-immutable-pure-component';
 | |
| import LoadGap from './load_gap';
 | |
| import ScrollableList from './scrollable_list';
 | |
| 
 | |
| export default class StatusList extends ImmutablePureComponent {
 | |
| 
 | |
|   static propTypes = {
 | |
|     scrollKey: PropTypes.string.isRequired,
 | |
|     statusIds: ImmutablePropTypes.list.isRequired,
 | |
|     featuredStatusIds: ImmutablePropTypes.list,
 | |
|     onLoadMore: PropTypes.func,
 | |
|     onScrollToTop: PropTypes.func,
 | |
|     onScroll: PropTypes.func,
 | |
|     trackScroll: PropTypes.bool,
 | |
|     shouldUpdateScroll: PropTypes.func,
 | |
|     isLoading: PropTypes.bool,
 | |
|     isPartial: PropTypes.bool,
 | |
|     hasMore: PropTypes.bool,
 | |
|     prepend: PropTypes.node,
 | |
|     emptyMessage: PropTypes.node,
 | |
|     alwaysPrepend: PropTypes.bool,
 | |
|     timelineId: PropTypes.string,
 | |
|   };
 | |
| 
 | |
|   static defaultProps = {
 | |
|     trackScroll: true,
 | |
|   };
 | |
| 
 | |
|   getFeaturedStatusCount = () => {
 | |
|     return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
 | |
|   }
 | |
| 
 | |
|   getCurrentStatusIndex = (id, featured) => {
 | |
|     if (featured) {
 | |
|       return this.props.featuredStatusIds.indexOf(id);
 | |
|     } else {
 | |
|       return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   handleMoveUp = (id, featured) => {
 | |
|     const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
 | |
|     this._selectChild(elementIndex);
 | |
|   }
 | |
| 
 | |
|   handleMoveDown = (id, featured) => {
 | |
|     const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
 | |
|     this._selectChild(elementIndex);
 | |
|   }
 | |
| 
 | |
|   handleLoadOlder = debounce(() => {
 | |
|     this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
 | |
|   }, 300, { leading: true })
 | |
| 
 | |
|   _selectChild (index) {
 | |
|     const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
 | |
| 
 | |
|     if (element) {
 | |
|       element.focus();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   setRef = c => {
 | |
|     this.node = c;
 | |
|   }
 | |
| 
 | |
|   render () {
 | |
|     const { statusIds, featuredStatusIds, shouldUpdateScroll, onLoadMore, timelineId, ...other }  = this.props;
 | |
|     const { isLoading, isPartial } = other;
 | |
| 
 | |
|     if (isPartial) {
 | |
|       return (
 | |
|         <div className='regeneration-indicator'>
 | |
|           <div>
 | |
|             <div className='regeneration-indicator__figure' />
 | |
| 
 | |
|             <div className='regeneration-indicator__label'>
 | |
|               <FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' />
 | |
|               <FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let scrollableContent = (isLoading || statusIds.size > 0) ? (
 | |
|       statusIds.map((statusId, index) => statusId === null ? (
 | |
|         <LoadGap
 | |
|           key={'gap:' + statusIds.get(index + 1)}
 | |
|           disabled={isLoading}
 | |
|           maxId={index > 0 ? statusIds.get(index - 1) : null}
 | |
|           onClick={onLoadMore}
 | |
|         />
 | |
|       ) : (
 | |
|         <StatusContainer
 | |
|           key={statusId}
 | |
|           id={statusId}
 | |
|           onMoveUp={this.handleMoveUp}
 | |
|           onMoveDown={this.handleMoveDown}
 | |
|           contextType={timelineId}
 | |
|           showThread
 | |
|         />
 | |
|       ))
 | |
|     ) : null;
 | |
| 
 | |
|     if (scrollableContent && featuredStatusIds) {
 | |
|       scrollableContent = featuredStatusIds.map(statusId => (
 | |
|         <StatusContainer
 | |
|           key={`f-${statusId}`}
 | |
|           id={statusId}
 | |
|           featured
 | |
|           onMoveUp={this.handleMoveUp}
 | |
|           onMoveDown={this.handleMoveDown}
 | |
|           contextType={timelineId}
 | |
|           showThread
 | |
|         />
 | |
|       )).concat(scrollableContent);
 | |
|     }
 | |
| 
 | |
|     return (
 | |
|       <ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
 | |
|         {scrollableContent}
 | |
|       </ScrollableList>
 | |
|     );
 | |
|   }
 | |
| 
 | |
| }
 |