about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2016-11-13 20:42:54 +0100
committerEugen Rochko <eugen@zeonfederated.com>2016-11-13 20:42:54 +0100
commitdbfe1e4be6fb46c7374275a2465f4386798516cd (patch)
treeb597058c68aceed0c3487b39ba5c75cd64c6ee71
parent49b789695368e201eb90d504f48adaa16522bd7b (diff)
Infinite scroll for followers/following lists
-rw-r--r--app/assets/javascripts/components/actions/accounts.jsx181
-rw-r--r--app/assets/javascripts/components/api.jsx5
-rw-r--r--app/assets/javascripts/components/features/compose/components/upload_form.jsx1
-rw-r--r--app/assets/javascripts/components/features/followers/index.jsx33
-rw-r--r--app/assets/javascripts/components/features/following/index.jsx33
-rw-r--r--app/assets/javascripts/components/reducers/accounts.jsx4
-rw-r--r--app/assets/javascripts/components/reducers/user_lists.jsx25
-rw-r--r--package.json1
-rw-r--r--yarn.lock4
9 files changed, 219 insertions, 68 deletions
diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx
index fdfd204a1..c84d43221 100644
--- a/app/assets/javascripts/components/actions/accounts.jsx
+++ b/app/assets/javascripts/components/actions/accounts.jsx
@@ -1,4 +1,4 @@
-import api       from '../api'
+import api, { getLinks } from '../api'
 import Immutable from 'immutable';
 
 export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF';
@@ -35,10 +35,18 @@ export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
 export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
 export const FOLLOWERS_FETCH_FAIL    = 'FOLLOWERS_FETCH_FAIL';
 
+export const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST';
+export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS';
+export const FOLLOWERS_EXPAND_FAIL    = 'FOLLOWERS_EXPAND_FAIL';
+
 export const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST';
 export const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS';
 export const FOLLOWING_FETCH_FAIL    = 'FOLLOWING_FETCH_FAIL';
 
+export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST';
+export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
+export const FOLLOWING_EXPAND_FAIL    = 'FOLLOWING_EXPAND_FAIL';
+
 export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
 export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
 export const RELATIONSHIPS_FETCH_FAIL    = 'RELATIONSHIPS_FETCH_FAIL';
@@ -46,7 +54,7 @@ export const RELATIONSHIPS_FETCH_FAIL    = 'RELATIONSHIPS_FETCH_FAIL';
 export function setAccountSelf(account) {
   return {
     type: ACCOUNT_SET_SELF,
-    account: account
+    account
   };
 };
 
@@ -101,22 +109,22 @@ export function expandAccountTimeline(id) {
 export function fetchAccountRequest(id) {
   return {
     type: ACCOUNT_FETCH_REQUEST,
-    id: id
+    id
   };
 };
 
 export function fetchAccountSuccess(account) {
   return {
     type: ACCOUNT_FETCH_SUCCESS,
-    account: account
+    account
   };
 };
 
 export function fetchAccountFail(id, error) {
   return {
     type: ACCOUNT_FETCH_FAIL,
-    id: id,
-    error: error
+    id,
+    error
   };
 };
 
@@ -147,89 +155,89 @@ export function unfollowAccount(id) {
 export function followAccountRequest(id) {
   return {
     type: ACCOUNT_FOLLOW_REQUEST,
-    id: id
+    id
   };
 };
 
 export function followAccountSuccess(relationship) {
   return {
     type: ACCOUNT_FOLLOW_SUCCESS,
-    relationship: relationship
+    relationship
   };
 };
 
 export function followAccountFail(error) {
   return {
     type: ACCOUNT_FOLLOW_FAIL,
-    error: error
+    error
   };
 };
 
 export function unfollowAccountRequest(id) {
   return {
     type: ACCOUNT_UNFOLLOW_REQUEST,
-    id: id
+    id
   };
 };
 
 export function unfollowAccountSuccess(relationship) {
   return {
     type: ACCOUNT_UNFOLLOW_SUCCESS,
-    relationship: relationship
+    relationship
   };
 };
 
 export function unfollowAccountFail(error) {
   return {
     type: ACCOUNT_UNFOLLOW_FAIL,
-    error: error
+    error
   };
 };
 
 export function fetchAccountTimelineRequest(id) {
   return {
     type: ACCOUNT_TIMELINE_FETCH_REQUEST,
-    id: id
+    id
   };
 };
 
 export function fetchAccountTimelineSuccess(id, statuses, replace) {
   return {
     type: ACCOUNT_TIMELINE_FETCH_SUCCESS,
-    id: id,
-    statuses: statuses,
-    replace: replace
+    id,
+    statuses,
+    replace
   };
 };
 
 export function fetchAccountTimelineFail(id, error) {
   return {
     type: ACCOUNT_TIMELINE_FETCH_FAIL,
-    id: id,
-    error: error
+    id,
+    error
   };
 };
 
 export function expandAccountTimelineRequest(id) {
   return {
     type: ACCOUNT_TIMELINE_EXPAND_REQUEST,
-    id: id
+    id
   };
 };
 
 export function expandAccountTimelineSuccess(id, statuses) {
   return {
     type: ACCOUNT_TIMELINE_EXPAND_SUCCESS,
-    id: id,
-    statuses: statuses
+    id,
+    statuses
   };
 };
 
 export function expandAccountTimelineFail(id, error) {
   return {
     type: ACCOUNT_TIMELINE_EXPAND_FAIL,
-    id: id,
-    error: error
+    id,
+    error
   };
 };
 
@@ -260,42 +268,42 @@ export function unblockAccount(id) {
 export function blockAccountRequest(id) {
   return {
     type: ACCOUNT_BLOCK_REQUEST,
-    id: id
+    id
   };
 };
 
 export function blockAccountSuccess(relationship) {
   return {
     type: ACCOUNT_BLOCK_SUCCESS,
-    relationship: relationship
+    relationship
   };
 };
 
 export function blockAccountFail(error) {
   return {
     type: ACCOUNT_BLOCK_FAIL,
-    error: error
+    error
   };
 };
 
 export function unblockAccountRequest(id) {
   return {
     type: ACCOUNT_UNBLOCK_REQUEST,
-    id: id
+    id
   };
 };
 
 export function unblockAccountSuccess(relationship) {
   return {
     type: ACCOUNT_UNBLOCK_SUCCESS,
-    relationship: relationship
+    relationship
   };
 };
 
 export function unblockAccountFail(error) {
   return {
     type: ACCOUNT_UNBLOCK_FAIL,
-    error: error
+    error
   };
 };
 
@@ -304,7 +312,9 @@ export function fetchFollowers(id) {
     dispatch(fetchFollowersRequest(id));
 
     api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
-      dispatch(fetchFollowersSuccess(id, response.data));
+      const prev = getLinks(response).refs.find(link => link.rel === 'prev').uri;
+
+      dispatch(fetchFollowersSuccess(id, response.data, prev));
       dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
       dispatch(fetchFollowersFail(id, error));
@@ -315,23 +325,65 @@ export function fetchFollowers(id) {
 export function fetchFollowersRequest(id) {
   return {
     type: FOLLOWERS_FETCH_REQUEST,
-    id: id
+    id
   };
 };
 
-export function fetchFollowersSuccess(id, accounts) {
+export function fetchFollowersSuccess(id, accounts, prev) {
   return {
     type: FOLLOWERS_FETCH_SUCCESS,
-    id: id,
-    accounts: accounts
+    id,
+    accounts,
+    prev
   };
 };
 
 export function fetchFollowersFail(id, error) {
   return {
     type: FOLLOWERS_FETCH_FAIL,
-    id: id,
-    error: error
+    id,
+    error
+  };
+};
+
+export function expandFollowers(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'followers', id, 'prev']);
+
+    dispatch(expandFollowersRequest(id));
+
+    api(getState).get(url).then(response => {
+      const prev = getLinks(response).refs.find(link => link.rel === 'prev').uri;
+
+      dispatch(expandFollowersSuccess(id, response.data, prev));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(expandFollowersFail(id, error));
+    });
+  };
+};
+
+export function expandFollowersRequest(id) {
+  return {
+    type: FOLLOWERS_EXPAND_REQUEST,
+    id
+  };
+};
+
+export function expandFollowersSuccess(id, accounts, prev) {
+  return {
+    type: FOLLOWERS_EXPAND_SUCCESS,
+    id,
+    accounts,
+    prev
+  };
+};
+
+export function expandFollowersFail(id, error) {
+  return {
+    type: FOLLOWERS_EXPAND_FAIL,
+    id,
+    error
   };
 };
 
@@ -351,23 +403,64 @@ export function fetchFollowing(id) {
 export function fetchFollowingRequest(id) {
   return {
     type: FOLLOWING_FETCH_REQUEST,
-    id: id
+    id
   };
 };
 
 export function fetchFollowingSuccess(id, accounts) {
   return {
     type: FOLLOWING_FETCH_SUCCESS,
-    id: id,
-    accounts: accounts
+    id,
+    accounts
   };
 };
 
 export function fetchFollowingFail(id, error) {
   return {
     type: FOLLOWING_FETCH_FAIL,
-    id: id,
-    error: error
+    id,
+    error
+  };
+};
+
+export function expandFollowing(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'following', id, 'prev']);
+
+    dispatch(expandFollowingRequest(id));
+
+    api(getState).get(url).then(response => {
+      const prev = getLinks(response).refs.find(link => link.rel === 'prev').uri;
+
+      dispatch(expandFollowingSuccess(id, response.data, prev));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(expandFollowingFail(id, error));
+    });
+  };
+};
+
+export function expandFollowingRequest(id) {
+  return {
+    type: FOLLOWING_EXPAND_REQUEST,
+    id
+  };
+};
+
+export function expandFollowingSuccess(id, accounts, prev) {
+  return {
+    type: FOLLOWING_EXPAND_SUCCESS,
+    id,
+    accounts,
+    prev
+  };
+};
+
+export function expandFollowingFail(id, error) {
+  return {
+    type: FOLLOWING_EXPAND_FAIL,
+    id,
+    error
   };
 };
 
@@ -386,20 +479,20 @@ export function fetchRelationships(account_ids) {
 export function fetchRelationshipsRequest(ids) {
   return {
     type: RELATIONSHIPS_FETCH_REQUEST,
-    ids: ids
+    ids
   };
 };
 
 export function fetchRelationshipsSuccess(relationships) {
   return {
     type: RELATIONSHIPS_FETCH_SUCCESS,
-    relationships: relationships
+    relationships
   };
 };
 
 export function fetchRelationshipsFail(error) {
   return {
     type: RELATIONSHIPS_FETCH_FAIL,
-    error: error
+    error
   };
 };
diff --git a/app/assets/javascripts/components/api.jsx b/app/assets/javascripts/components/api.jsx
index f317af094..f674290ab 100644
--- a/app/assets/javascripts/components/api.jsx
+++ b/app/assets/javascripts/components/api.jsx
@@ -1,4 +1,9 @@
 import axios from 'axios';
+import LinkHeader from 'http-link-header';
+
+export const getLinks = response => {
+  return LinkHeader.parse(response.headers.link);
+};
 
 export default getState => axios.create({
   headers: {
diff --git a/app/assets/javascripts/components/features/compose/components/upload_form.jsx b/app/assets/javascripts/components/features/compose/components/upload_form.jsx
index 751f76ab7..eab504b48 100644
--- a/app/assets/javascripts/components/features/compose/components/upload_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/upload_form.jsx
@@ -7,7 +7,6 @@ const UploadForm = React.createClass({
   propTypes: {
     media: ImmutablePropTypes.list.isRequired,
     is_uploading: React.PropTypes.bool,
-    onSelectFile: React.PropTypes.func.isRequired,
     onRemoveFile: React.PropTypes.func.isRequired
   },
 
diff --git a/app/assets/javascripts/components/features/followers/index.jsx b/app/assets/javascripts/components/features/followers/index.jsx
index ff3f97b09..13eed69ca 100644
--- a/app/assets/javascripts/components/features/followers/index.jsx
+++ b/app/assets/javascripts/components/features/followers/index.jsx
@@ -1,13 +1,16 @@
-import { connect }            from 'react-redux';
-import PureRenderMixin        from 'react-addons-pure-render-mixin';
-import ImmutablePropTypes     from 'react-immutable-proptypes';
-import LoadingIndicator       from '../../components/loading_indicator';
-import { fetchFollowers }     from '../../actions/accounts';
-import { ScrollContainer }    from 'react-router-scroll';
-import AccountContainer       from './containers/account_container';
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import {
+  fetchFollowers,
+  expandFollowers
+} from '../../actions/accounts';
+import { ScrollContainer } from 'react-router-scroll';
+import AccountContainer from './containers/account_container';
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId)])
+  accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items'])
 });
 
 const Followers = React.createClass({
@@ -30,6 +33,14 @@ const Followers = React.createClass({
     }
   },
 
+  handleScroll (e) {
+    const { scrollTop, scrollHeight, clientHeight } = e.target;
+
+    if (scrollTop === scrollHeight - clientHeight) {
+      this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
+    }
+  },
+
   render () {
     const { accountIds } = this.props;
 
@@ -39,8 +50,10 @@ const Followers = React.createClass({
 
     return (
       <ScrollContainer scrollKey='followers'>
-        <div className='scrollable'>
-          {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+        <div className='scrollable' onScroll={this.handleScroll}>
+          <div>
+            {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+          </div>
         </div>
       </ScrollContainer>
     );
diff --git a/app/assets/javascripts/components/features/following/index.jsx b/app/assets/javascripts/components/features/following/index.jsx
index bd3c3bd45..865b39736 100644
--- a/app/assets/javascripts/components/features/following/index.jsx
+++ b/app/assets/javascripts/components/features/following/index.jsx
@@ -1,13 +1,16 @@
-import { connect }            from 'react-redux';
-import PureRenderMixin        from 'react-addons-pure-render-mixin';
-import ImmutablePropTypes     from 'react-immutable-proptypes';
-import LoadingIndicator       from '../../components/loading_indicator';
-import { fetchFollowing }     from '../../actions/accounts';
-import { ScrollContainer }    from 'react-router-scroll';
-import AccountContainer       from '../followers/containers/account_container';
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import {
+  fetchFollowing,
+  expandFollowing
+} from '../../actions/accounts';
+import { ScrollContainer } from 'react-router-scroll';
+import AccountContainer from '../followers/containers/account_container';
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId)])
+  accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items'])
 });
 
 const Following = React.createClass({
@@ -30,6 +33,14 @@ const Following = React.createClass({
     }
   },
 
+  handleScroll (e) {
+    const { scrollTop, scrollHeight, clientHeight } = e.target;
+
+    if (scrollTop === scrollHeight - clientHeight) {
+      this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
+    }
+  },
+
   render () {
     const { accountIds } = this.props;
 
@@ -39,8 +50,10 @@ const Following = React.createClass({
 
     return (
       <ScrollContainer scrollKey='following'>
-        <div className='scrollable'>
-          {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+        <div className='scrollable' onScroll={this.handleScroll}>
+          <div>
+            {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+          </div>
         </div>
       </ScrollContainer>
     );
diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx
index c380a88f0..c0ea961b7 100644
--- a/app/assets/javascripts/components/reducers/accounts.jsx
+++ b/app/assets/javascripts/components/reducers/accounts.jsx
@@ -2,7 +2,9 @@ import {
   ACCOUNT_SET_SELF,
   ACCOUNT_FETCH_SUCCESS,
   FOLLOWERS_FETCH_SUCCESS,
+  FOLLOWERS_EXPAND_SUCCESS,
   FOLLOWING_FETCH_SUCCESS,
+  FOLLOWING_EXPAND_SUCCESS,
   ACCOUNT_TIMELINE_FETCH_SUCCESS,
   ACCOUNT_TIMELINE_EXPAND_SUCCESS
 } from '../actions/accounts';
@@ -65,7 +67,9 @@ export default function accounts(state = initialState, action) {
       return normalizeAccount(state, action.account);
     case SUGGESTIONS_FETCH_SUCCESS:
     case FOLLOWERS_FETCH_SUCCESS:
+    case FOLLOWERS_EXPAND_SUCCESS:
     case FOLLOWING_FETCH_SUCCESS:
+    case FOLLOWING_EXPAND_SUCCESS:
     case REBLOGS_FETCH_SUCCESS:
     case FAVOURITES_FETCH_SUCCESS:
     case COMPOSE_SUGGESTIONS_READY:
diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx
index 4c201f927..de5c85bba 100644
--- a/app/assets/javascripts/components/reducers/user_lists.jsx
+++ b/app/assets/javascripts/components/reducers/user_lists.jsx
@@ -1,6 +1,8 @@
 import {
   FOLLOWERS_FETCH_SUCCESS,
-  FOLLOWING_FETCH_SUCCESS
+  FOLLOWERS_EXPAND_SUCCESS,
+  FOLLOWING_FETCH_SUCCESS,
+  FOLLOWING_EXPAND_SUCCESS
 } from '../actions/accounts';
 import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions';
 import {
@@ -17,12 +19,29 @@ const initialState = Immutable.Map({
   favourited_by: Immutable.Map()
 });
 
+const normalizeList = (state, type, id, accounts, prev) => {
+  return state.setIn([type, id], Immutable.Map({
+    prev,
+    items: Immutable.List(accounts.map(item => item.id))
+  }));
+};
+
+const appendToList = (state, type, id, accounts, prev) => {
+  return state.updateIn([type, id], map => {
+    return map.set('prev', prev).update('items', list => list.push(...accounts.map(item => item.id)));
+  });
+};
+
 export default function userLists(state = initialState, action) {
   switch(action.type) {
     case FOLLOWERS_FETCH_SUCCESS:
-      return state.setIn(['followers', action.id], Immutable.List(action.accounts.map(item => item.id)));
+      return normalizeList(state, 'followers', action.id, action.accounts, action.prev);
+    case FOLLOWERS_EXPAND_SUCCESS:
+      return appendToList(state, 'followers', action.id, action.accounts, action.prev);
     case FOLLOWING_FETCH_SUCCESS:
-      return state.setIn(['following', action.id], Immutable.List(action.accounts.map(item => item.id)));
+      return normalizeList(state, 'following', action.id, action.accounts, action.prev);
+    case FOLLOWING_EXPAND_SUCCESS:
+      return appendToList(state, 'following', action.id, action.accounts, action.prev);
     case SUGGESTIONS_FETCH_SUCCESS:
       return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id)));
     case REBLOGS_FETCH_SUCCESS:
diff --git a/package.json b/package.json
index 388024abd..e514e03b9 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
   "dependencies": {
     "babel-plugin-transform-decorators-legacy": "^1.3.4",
     "emojione": "^2.2.6",
+    "http-link-header": "^0.5.0",
     "react-autosuggest": "^7.0.1",
     "react-decoration": "^1.4.0",
     "react-motion": "^0.4.5",
diff --git a/yarn.lock b/yarn.lock
index 5b4230778..55f151753 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2399,6 +2399,10 @@ http-errors@~1.5.0:
     setprototypeof "1.0.1"
     statuses ">= 1.3.0 < 2"
 
+http-link-header:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-0.5.0.tgz#68598d92c55d3dac7d3e6ae405142fecf7bd3303"
+
 http-signature@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"