Infinite scroll for timeline columns
This commit is contained in:
		
							parent
							
								
									74dfefabd3
								
							
						
					
					
						commit
						2c0261ac25
					
				
					 5 changed files with 86 additions and 5 deletions
				
			
		|  | @ -60,3 +60,40 @@ export function refreshTimelineFail(timeline, error) { | |||
|     error: error | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandTimeline(timeline) { | ||||
|   return (dispatch, getState) => { | ||||
|     const lastId = getState().getIn(['timelines', timeline]).last(); | ||||
| 
 | ||||
|     dispatch(expandTimelineRequest(timeline)); | ||||
| 
 | ||||
|     api(getState).get(`/api/statuses/${timeline}?max_id=${lastId}`).then(response => { | ||||
|       dispatch(expandTimelineSuccess(timeline, response.data)); | ||||
|     }).catch(error => { | ||||
|       dispatch(expandTimelineFail(timeline, error)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandTimelineRequest(timeline) { | ||||
|   return { | ||||
|     type: TIMELINE_EXPAND_REQUEST, | ||||
|     timeline: timeline | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandTimelineSuccess(timeline, statuses) { | ||||
|   return { | ||||
|     type: TIMELINE_EXPAND_SUCCESS, | ||||
|     timeline: timeline, | ||||
|     statuses: statuses | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandTimelineFail(timeline, error) { | ||||
|   return { | ||||
|     type: TIMELINE_EXPAND_FAIL, | ||||
|     timeline: timeline, | ||||
|     error: error | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -8,14 +8,23 @@ const StatusList = React.createClass({ | |||
|     statuses: ImmutablePropTypes.list.isRequired, | ||||
|     onReply: React.PropTypes.func, | ||||
|     onReblog: React.PropTypes.func, | ||||
|     onFavourite: React.PropTypes.func | ||||
|     onFavourite: React.PropTypes.func, | ||||
|     onScrollToBottom: React.PropTypes.func | ||||
|   }, | ||||
| 
 | ||||
|   mixins: [PureRenderMixin], | ||||
| 
 | ||||
|   handleScroll (e) { | ||||
|     const { scrollTop, scrollHeight, clientHeight } = e.target; | ||||
| 
 | ||||
|     if (scrollTop === scrollHeight - clientHeight) { | ||||
|       this.props.onScrollToBottom(); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   render () { | ||||
|     return ( | ||||
|       <div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'> | ||||
|       <div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable' onScroll={this.handleScroll}> | ||||
|         <div> | ||||
|           {this.props.statuses.map((status) => { | ||||
|             return <Status key={status.get('id')} status={status} onReply={this.props.onReply} onReblog={this.props.onReblog} onFavourite={this.props.onFavourite} />; | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { connect }           from 'react-redux'; | |||
| import StatusList            from '../../../components/status_list'; | ||||
| import { replyCompose }      from '../../../actions/compose'; | ||||
| import { reblog, favourite } from '../../../actions/interactions'; | ||||
| import { expandTimeline }    from '../../../actions/timelines'; | ||||
| import { selectStatus }      from '../../../reducers/timelines'; | ||||
| 
 | ||||
| const mapStateToProps = function (state, props) { | ||||
|  | @ -10,7 +11,7 @@ const mapStateToProps = function (state, props) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = function (dispatch) { | ||||
| const mapDispatchToProps = function (dispatch, props) { | ||||
|   return { | ||||
|     onReply: function (status) { | ||||
|       dispatch(replyCompose(status)); | ||||
|  | @ -22,6 +23,10 @@ const mapDispatchToProps = function (dispatch) { | |||
| 
 | ||||
|     onReblog: function (status) { | ||||
|       dispatch(reblog(status)); | ||||
|     }, | ||||
| 
 | ||||
|     onScrollToBottom: function () { | ||||
|       dispatch(expandTimeline(props.type)); | ||||
|     } | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -1,8 +1,18 @@ | |||
| import { COMPOSE_SUBMIT_FAIL, COMPOSE_UPLOAD_FAIL } from '../actions/compose'; | ||||
| import { FOLLOW_SUBMIT_FAIL }                       from '../actions/follow'; | ||||
| import { REBLOG_FAIL, FAVOURITE_FAIL }              from '../actions/interactions'; | ||||
| import { TIMELINE_REFRESH_FAIL }                    from '../actions/timelines'; | ||||
| import { | ||||
|   TIMELINE_REFRESH_FAIL, | ||||
|   TIMELINE_EXPAND_FAIL | ||||
| }                                                   from '../actions/timelines'; | ||||
| import { NOTIFICATION_DISMISS, NOTIFICATION_CLEAR } from '../actions/notifications'; | ||||
| import { | ||||
|   ACCOUNT_FETCH_FAIL, | ||||
|   ACCOUNT_FOLLOW_FAIL, | ||||
|   ACCOUNT_UNFOLLOW_FAIL, | ||||
|   ACCOUNT_TIMELINE_FETCH_FAIL | ||||
| }                                                   from '../actions/accounts'; | ||||
| import { STATUS_FETCH_FAIL }                        from '../actions/statuses'; | ||||
| import Immutable                                    from 'immutable'; | ||||
| 
 | ||||
| const initialState = Immutable.List(); | ||||
|  | @ -33,6 +43,12 @@ export default function notifications(state = initialState, action) { | |||
|     case REBLOG_FAIL: | ||||
|     case FAVOURITE_FAIL: | ||||
|     case TIMELINE_REFRESH_FAIL: | ||||
|     case TIMELINE_EXPAND_FAIL: | ||||
|     case ACCOUNT_FETCH_FAIL: | ||||
|     case ACCOUNT_FOLLOW_FAIL: | ||||
|     case ACCOUNT_UNFOLLOW_FAIL: | ||||
|     case ACCOUNT_TIMELINE_FETCH_FAIL: | ||||
|     case STATUS_FETCH_FAIL: | ||||
|       return notificationFromError(state, action.error); | ||||
|     case NOTIFICATION_DISMISS: | ||||
|       return state.filterNot(item => item.get('key') === action.notification.key); | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| import { | ||||
|   TIMELINE_REFRESH_SUCCESS, | ||||
|   TIMELINE_UPDATE, | ||||
|   TIMELINE_DELETE | ||||
|   TIMELINE_DELETE, | ||||
|   TIMELINE_EXPAND_SUCCESS | ||||
| }                                from '../actions/timelines'; | ||||
| import { | ||||
|   REBLOG_SUCCESS, | ||||
|  | @ -89,6 +90,17 @@ function normalizeTimeline(state, timeline, statuses) { | |||
|   return state; | ||||
| }; | ||||
| 
 | ||||
| function appendNormalizedTimeline(state, timeline, statuses) { | ||||
|   let moreIds = Immutable.List(); | ||||
| 
 | ||||
|   statuses.forEach((status, i) => { | ||||
|     state   = normalizeStatus(state, status); | ||||
|     moreIds = moreIds.set(i, status.get('id')); | ||||
|   }); | ||||
| 
 | ||||
|   return state.update(timeline, list => list.push(...moreIds)); | ||||
| }; | ||||
| 
 | ||||
| function normalizeAccountTimeline(state, accountId, statuses) { | ||||
|   statuses.forEach((status, i) => { | ||||
|     state = normalizeStatus(state, status); | ||||
|  | @ -141,6 +153,8 @@ export default function timelines(state = initialState, action) { | |||
|   switch(action.type) { | ||||
|     case TIMELINE_REFRESH_SUCCESS: | ||||
|       return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); | ||||
|     case TIMELINE_EXPAND_SUCCESS: | ||||
|       return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); | ||||
|     case TIMELINE_UPDATE: | ||||
|       return updateTimeline(state, action.timeline, Immutable.fromJS(action.status)); | ||||
|     case TIMELINE_DELETE: | ||||
|  |  | |||
		Reference in a new issue