Remember scroll position when navigating back, do not needlessly reload
entire timelines (only fetch since last known ID). Side effect: account timelines no longer update in real-time
This commit is contained in:
		
							parent
							
								
									39aa7caaa5
								
							
						
					
					
						commit
						fac770fccd
					
				
					 10 changed files with 76 additions and 20 deletions
				
			
		|  | @ -57,7 +57,16 @@ export function fetchAccountTimeline(id) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|     dispatch(fetchAccountTimelineRequest(id)); |     dispatch(fetchAccountTimelineRequest(id)); | ||||||
| 
 | 
 | ||||||
|     api(getState).get(`/api/v1/accounts/${id}/statuses`).then(response => { |     const ids      = getState().getIn(['timelines', 'accounts_timelines', id], Immutable.List()); | ||||||
|  |     const newestId = ids.size > 0 ? ids.first() : null; | ||||||
|  | 
 | ||||||
|  |     let params = ''; | ||||||
|  | 
 | ||||||
|  |     if (newestId !== null) { | ||||||
|  |       params = `?since_id=${newestId}`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     api(getState).get(`/api/v1/accounts/${id}/statuses${params}`).then(response => { | ||||||
|       dispatch(fetchAccountTimelineSuccess(id, response.data)); |       dispatch(fetchAccountTimelineSuccess(id, response.data)); | ||||||
|     }).catch(error => { |     }).catch(error => { | ||||||
|       dispatch(fetchAccountTimelineFail(id, error)); |       dispatch(fetchAccountTimelineFail(id, error)); | ||||||
|  |  | ||||||
|  | @ -45,7 +45,16 @@ export function refreshTimeline(timeline) { | ||||||
|   return function (dispatch, getState) { |   return function (dispatch, getState) { | ||||||
|     dispatch(refreshTimelineRequest(timeline)); |     dispatch(refreshTimelineRequest(timeline)); | ||||||
| 
 | 
 | ||||||
|     api(getState).get(`/api/v1/statuses/${timeline}`).then(function (response) { |     const ids      = getState().getIn(['timelines', timeline]); | ||||||
|  |     const newestId = ids.size > 0 ? ids.first() : null; | ||||||
|  | 
 | ||||||
|  |     let params = ''; | ||||||
|  | 
 | ||||||
|  |     if (newestId !== null) { | ||||||
|  |       params = `?since_id=${newestId}`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     api(getState).get(`/api/v1/statuses/${timeline}${params}`).then(function (response) { | ||||||
|       dispatch(refreshTimelineSuccess(timeline, response.data)); |       dispatch(refreshTimelineSuccess(timeline, response.data)); | ||||||
|     }).catch(function (error) { |     }).catch(function (error) { | ||||||
|       dispatch(refreshTimelineFail(timeline, error)); |       dispatch(refreshTimelineFail(timeline, error)); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import Status             from './status'; | import Status              from './status'; | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes  from 'react-immutable-proptypes'; | ||||||
| import PureRenderMixin    from 'react-addons-pure-render-mixin'; | import PureRenderMixin     from 'react-addons-pure-render-mixin'; | ||||||
|  | import { ScrollContainer } from 'react-router-scroll'; | ||||||
| 
 | 
 | ||||||
| const StatusList = React.createClass({ | const StatusList = React.createClass({ | ||||||
| 
 | 
 | ||||||
|  | @ -11,9 +12,16 @@ const StatusList = React.createClass({ | ||||||
|     onFavourite: React.PropTypes.func, |     onFavourite: React.PropTypes.func, | ||||||
|     onDelete: React.PropTypes.func, |     onDelete: React.PropTypes.func, | ||||||
|     onScrollToBottom: React.PropTypes.func, |     onScrollToBottom: React.PropTypes.func, | ||||||
|  |     trackScroll: React.PropTypes.bool, | ||||||
|     me: React.PropTypes.number |     me: React.PropTypes.number | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|  |   getDefaultProps () { | ||||||
|  |     return { | ||||||
|  |       trackScroll: true | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|   mixins: [PureRenderMixin], |   mixins: [PureRenderMixin], | ||||||
| 
 | 
 | ||||||
|   handleScroll (e) { |   handleScroll (e) { | ||||||
|  | @ -25,9 +33,9 @@ const StatusList = React.createClass({ | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { statuses, onScrollToBottom, ...other } = this.props; |     const { statuses, onScrollToBottom, trackScroll, ...other } = this.props; | ||||||
| 
 | 
 | ||||||
|     return ( |     const scrollableArea = ( | ||||||
|       <div style={{ overflowY: 'scroll', flex: '1 1 auto', overflowX: 'hidden' }} className='scrollable' onScroll={this.handleScroll}> |       <div style={{ overflowY: 'scroll', flex: '1 1 auto', overflowX: 'hidden' }} className='scrollable' onScroll={this.handleScroll}> | ||||||
|         <div> |         <div> | ||||||
|           {statuses.map((status) => { |           {statuses.map((status) => { | ||||||
|  | @ -36,6 +44,16 @@ const StatusList = React.createClass({ | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|  | 
 | ||||||
|  |     if (trackScroll) { | ||||||
|  |       return ( | ||||||
|  |         <ScrollContainer scrollKey='status-list'> | ||||||
|  |           {scrollableArea} | ||||||
|  |         </ScrollContainer> | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return scrollableArea; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -10,11 +10,13 @@ import { setAccessToken } from '../actions/meta'; | ||||||
| import { setAccountSelf } from '../actions/accounts'; | import { setAccountSelf } from '../actions/accounts'; | ||||||
| import PureRenderMixin    from 'react-addons-pure-render-mixin'; | import PureRenderMixin    from 'react-addons-pure-render-mixin'; | ||||||
| import { | import { | ||||||
|  |   applyRouterMiddleware, | ||||||
|   Router, |   Router, | ||||||
|   Route, |   Route, | ||||||
|   hashHistory, |   hashHistory, | ||||||
|   IndexRoute |   IndexRoute | ||||||
| }                         from 'react-router'; | }                         from 'react-router'; | ||||||
|  | import { useScroll }      from 'react-router-scroll'; | ||||||
| import UI                 from '../features/ui'; | import UI                 from '../features/ui'; | ||||||
| import Account            from '../features/account'; | import Account            from '../features/account'; | ||||||
| import Status             from '../features/status'; | import Status             from '../features/status'; | ||||||
|  | @ -71,7 +73,7 @@ const Mastodon = React.createClass({ | ||||||
|   render () { |   render () { | ||||||
|     return ( |     return ( | ||||||
|       <Provider store={store}> |       <Provider store={store}> | ||||||
|         <Router history={hashHistory}> |         <Router history={hashHistory} render={applyRouterMiddleware(useScroll())}> | ||||||
|           <Route path='/' component={UI}> |           <Route path='/' component={UI}> | ||||||
|             <IndexRoute component={GettingStarted} /> |             <IndexRoute component={GettingStarted} /> | ||||||
|             <Route path='/statuses/new' component={Compose} /> |             <Route path='/statuses/new' component={Compose} /> | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ const HomeTimeline = React.createClass({ | ||||||
|   render () { |   render () { | ||||||
|     return ( |     return ( | ||||||
|       <Column icon='home' heading='Home'> |       <Column icon='home' heading='Home'> | ||||||
|         <StatusListContainer type='home' /> |         <StatusListContainer {...this.props} type='home' /> | ||||||
|       </Column> |       </Column> | ||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ const MentionsTimeline = React.createClass({ | ||||||
|   render () { |   render () { | ||||||
|     return ( |     return ( | ||||||
|       <Column icon='at' heading='Mentions'> |       <Column icon='at' heading='Mentions'> | ||||||
|         <StatusListContainer type='mentions' /> |         <StatusListContainer {...this.props} type='mentions' /> | ||||||
|       </Column> |       </Column> | ||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  | @ -28,8 +28,8 @@ const UI = React.createClass({ | ||||||
|         <MediaQuery minWidth={layoutBreakpoint}> |         <MediaQuery minWidth={layoutBreakpoint}> | ||||||
|           <ColumnsArea> |           <ColumnsArea> | ||||||
|             <Compose /> |             <Compose /> | ||||||
|             <HomeTimeline /> |             <HomeTimeline trackScroll={false} /> | ||||||
|             <MentionsTimeline /> |             <MentionsTimeline trackScroll={false} /> | ||||||
|             {this.props.children} |             {this.props.children} | ||||||
|           </ColumnsArea> |           </ColumnsArea> | ||||||
|         </MediaQuery> |         </MediaQuery> | ||||||
|  |  | ||||||
|  | @ -85,7 +85,7 @@ function normalizeTimeline(state, timeline, statuses) { | ||||||
|     ids   = ids.set(i, status.get('id')); |     ids   = ids.set(i, status.get('id')); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return state.set(timeline, ids); |   return state.update(timeline, list => list.unshift(...ids)); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function appendNormalizedTimeline(state, timeline, statuses) { | function appendNormalizedTimeline(state, timeline, statuses) { | ||||||
|  | @ -100,16 +100,14 @@ function appendNormalizedTimeline(state, timeline, statuses) { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function normalizeAccountTimeline(state, accountId, statuses) { | function normalizeAccountTimeline(state, accountId, statuses) { | ||||||
|   state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => { |   let ids = Immutable.List([]); | ||||||
|     return (list.size > 0) ? list.clear() : list; |  | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|   statuses.forEach((status, i) => { |   statuses.forEach((status, i) => { | ||||||
|     state = normalizeStatus(state, status); |     state = normalizeStatus(state, status); | ||||||
|     state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.set(i, status.get('id'))); |     ids   = ids.set(i, status.get('id')); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return state; |   return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.unshift(...ids)); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function appendNormalizedAccountTimeline(state, accountId, statuses) { | function appendNormalizedAccountTimeline(state, accountId, statuses) { | ||||||
|  | @ -137,7 +135,7 @@ function updateTimeline(state, timeline, status) { | ||||||
|     return list.unshift(status.get('id')); |     return list.unshift(status.get('id')); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   state = state.updateIn(['accounts_timelines', status.getIn(['account', 'id'])], Immutable.List([]), list => (list.includes(status.get('id')) ? list : list.unshift(status.get('id')))); |   //state = state.updateIn(['accounts_timelines', status.getIn(['account', 'id'])], Immutable.List([]), list => (list.includes(status.get('id')) ? list : list.unshift(status.get('id')))); | ||||||
| 
 | 
 | ||||||
|   return state; |   return state; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -41,6 +41,7 @@ | ||||||
|     "sinon": "^1.17.6" |     "sinon": "^1.17.6" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "react-responsive": "^1.1.5" |     "react-responsive": "^1.1.5", | ||||||
|  |     "react-router-scroll": "^0.3.2" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								yarn.lock
									
									
									
									
									
								
							|  | @ -1743,6 +1743,10 @@ diffie-hellman@^5.0.0: | ||||||
|     miller-rabin "^4.0.0" |     miller-rabin "^4.0.0" | ||||||
|     randombytes "^2.0.0" |     randombytes "^2.0.0" | ||||||
| 
 | 
 | ||||||
|  | dom-helpers@^2.4.0: | ||||||
|  |   version "2.4.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-2.4.0.tgz#9bb4b245f637367b1fa670274272aa28fe06c367" | ||||||
|  | 
 | ||||||
| dom-serializer@~0.1.0, dom-serializer@0: | dom-serializer@~0.1.0, dom-serializer@0: | ||||||
|   version "0.1.0" |   version "0.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" |   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" | ||||||
|  | @ -3892,6 +3896,14 @@ react-responsive: | ||||||
|     matchmedia "^0.1.2" |     matchmedia "^0.1.2" | ||||||
|     object-assign "^4.0.1" |     object-assign "^4.0.1" | ||||||
| 
 | 
 | ||||||
|  | react-router-scroll: | ||||||
|  |   version "0.3.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-router-scroll/-/react-router-scroll-0.3.2.tgz#ba8b1d01b3681dc5a68d72865d35c10e84065e52" | ||||||
|  |   dependencies: | ||||||
|  |     history "^2.1.2" | ||||||
|  |     scroll-behavior "^0.8.0" | ||||||
|  |     warning "^3.0.0" | ||||||
|  | 
 | ||||||
| react-router@^2.8.0: | react-router@^2.8.0: | ||||||
|   version "2.8.1" |   version "2.8.1" | ||||||
|   resolved "https://registry.yarnpkg.com/react-router/-/react-router-2.8.1.tgz#73e9491f6ceb316d0f779829081863e378ee4ed7" |   resolved "https://registry.yarnpkg.com/react-router/-/react-router-2.8.1.tgz#73e9491f6ceb316d0f779829081863e378ee4ed7" | ||||||
|  | @ -4147,6 +4159,13 @@ sax@^1.1.4, sax@~1.2.1: | ||||||
|   version "1.2.1" |   version "1.2.1" | ||||||
|   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" |   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" | ||||||
| 
 | 
 | ||||||
|  | scroll-behavior@^0.8.0: | ||||||
|  |   version "0.8.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/scroll-behavior/-/scroll-behavior-0.8.2.tgz#ace13e40b001d8d4d007aec0e7fb668cf9043546" | ||||||
|  |   dependencies: | ||||||
|  |     dom-helpers "^2.4.0" | ||||||
|  |     invariant "^2.2.1" | ||||||
|  | 
 | ||||||
| semver@~5.3.0: | semver@~5.3.0: | ||||||
|   version "5.3.0" |   version "5.3.0" | ||||||
|   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" |   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue