From 8698cd3281ac1d699c723a151b14f1e2f2e8b07e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 18 Oct 2016 23:06:28 +0200 Subject: [PATCH] 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 --- .../components/actions/accounts.jsx | 11 +++++++- .../components/actions/timelines.jsx | 11 +++++++- .../components/components/status_list.jsx | 28 +++++++++++++++---- .../components/containers/mastodon.jsx | 4 ++- .../features/home_timeline/index.jsx | 2 +- .../features/mentions_timeline/index.jsx | 2 +- .../components/features/ui/index.jsx | 4 +-- .../components/reducers/timelines.jsx | 12 ++++---- package.json | 3 +- yarn.lock | 19 +++++++++++++ 10 files changed, 76 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 4847c37e2..c1c99d6bd 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -57,7 +57,16 @@ export function fetchAccountTimeline(id) { return (dispatch, getState) => { 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)); }).catch(error => { dispatch(fetchAccountTimelineFail(id, error)); diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index f92f758f5..5258d7103 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -45,7 +45,16 @@ export function refreshTimeline(timeline) { return function (dispatch, getState) { 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)); }).catch(function (error) { dispatch(refreshTimelineFail(timeline, error)); diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx index f70d53263..4977d84ce 100644 --- a/app/assets/javascripts/components/components/status_list.jsx +++ b/app/assets/javascripts/components/components/status_list.jsx @@ -1,6 +1,7 @@ -import Status from './status'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; +import Status from './status'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import { ScrollContainer } from 'react-router-scroll'; const StatusList = React.createClass({ @@ -11,9 +12,16 @@ const StatusList = React.createClass({ onFavourite: React.PropTypes.func, onDelete: React.PropTypes.func, onScrollToBottom: React.PropTypes.func, + trackScroll: React.PropTypes.bool, me: React.PropTypes.number }, + getDefaultProps () { + return { + trackScroll: true + }; + }, + mixins: [PureRenderMixin], handleScroll (e) { @@ -25,9 +33,9 @@ const StatusList = React.createClass({ }, render () { - const { statuses, onScrollToBottom, ...other } = this.props; + const { statuses, onScrollToBottom, trackScroll, ...other } = this.props; - return ( + const scrollableArea = (
{statuses.map((status) => { @@ -36,6 +44,16 @@ const StatusList = React.createClass({
); + + if (trackScroll) { + return ( + + {scrollableArea} + + ); + } else { + return scrollableArea; + } } }); diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index e5c0887a9..4eb9f83c8 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -10,11 +10,13 @@ import { setAccessToken } from '../actions/meta'; import { setAccountSelf } from '../actions/accounts'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import { + applyRouterMiddleware, Router, Route, hashHistory, IndexRoute } from 'react-router'; +import { useScroll } from 'react-router-scroll'; import UI from '../features/ui'; import Account from '../features/account'; import Status from '../features/status'; @@ -71,7 +73,7 @@ const Mastodon = React.createClass({ render () { return ( - + diff --git a/app/assets/javascripts/components/features/home_timeline/index.jsx b/app/assets/javascripts/components/features/home_timeline/index.jsx index 9be3f3964..e4cd8bdca 100644 --- a/app/assets/javascripts/components/features/home_timeline/index.jsx +++ b/app/assets/javascripts/components/features/home_timeline/index.jsx @@ -19,7 +19,7 @@ const HomeTimeline = React.createClass({ render () { return ( - + ); }, diff --git a/app/assets/javascripts/components/features/mentions_timeline/index.jsx b/app/assets/javascripts/components/features/mentions_timeline/index.jsx index a1b511d3e..919a75d18 100644 --- a/app/assets/javascripts/components/features/mentions_timeline/index.jsx +++ b/app/assets/javascripts/components/features/mentions_timeline/index.jsx @@ -19,7 +19,7 @@ const MentionsTimeline = React.createClass({ render () { return ( - + ); }, diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index fab32a31e..06a9d2f50 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -28,8 +28,8 @@ const UI = React.createClass({ - - + + {this.props.children} diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index 65bccb44d..06534971d 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -85,7 +85,7 @@ function normalizeTimeline(state, timeline, statuses) { 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) { @@ -100,16 +100,14 @@ function appendNormalizedTimeline(state, timeline, statuses) { }; function normalizeAccountTimeline(state, accountId, statuses) { - state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => { - return (list.size > 0) ? list.clear() : list; - }); + let ids = Immutable.List([]); statuses.forEach((status, i) => { 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) { @@ -137,7 +135,7 @@ function updateTimeline(state, timeline, status) { 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; }; diff --git a/package.json b/package.json index 3d1878e15..78560f717 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "sinon": "^1.17.6" }, "dependencies": { - "react-responsive": "^1.1.5" + "react-responsive": "^1.1.5", + "react-router-scroll": "^0.3.2" } } diff --git a/yarn.lock b/yarn.lock index 353106819..6ee239818 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1743,6 +1743,10 @@ diffie-hellman@^5.0.0: miller-rabin "^4.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: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -3892,6 +3896,14 @@ react-responsive: matchmedia "^0.1.2" 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: version "2.8.1" 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" 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: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"