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
1d2175f73c
commit
8698cd3281
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