From 2a84271e85564928c4b5e241d7d3bde69fef40ed Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 22 Sep 2016 20:58:35 +0200 Subject: [PATCH] Infinite scroll for account timelines --- .../components/actions/accounts.jsx | 55 +++++++++++++++++-- .../components/features/account/index.jsx | 32 +++++++---- .../components/reducers/notifications.jsx | 4 +- .../components/reducers/timelines.jsx | 16 +++++- 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index aedc08a1d..ac9f5962f 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -1,4 +1,5 @@ -import api from '../api' +import api from '../api' +import axios from 'axios'; export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF'; @@ -18,6 +19,10 @@ export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST'; export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS'; export const ACCOUNT_TIMELINE_FETCH_FAIL = 'ACCOUNT_TIMELINE_FETCH_FAIL'; +export const ACCOUNT_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_TIMELINE_EXPAND_REQUEST'; +export const ACCOUNT_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_TIMELINE_EXPAND_SUCCESS'; +export const ACCOUNT_TIMELINE_EXPAND_FAIL = 'ACCOUNT_TIMELINE_EXPAND_FAIL'; + export function setAccountSelf(account) { return { type: ACCOUNT_SET_SELF, @@ -27,10 +32,12 @@ export function setAccountSelf(account) { export function fetchAccount(id) { return (dispatch, getState) => { + const boundApi = api(getState); + dispatch(fetchAccountRequest(id)); - api(getState).get(`/api/accounts/${id}`).then(response => { - dispatch(fetchAccountSuccess(response.data)); + axios.all([boundApi.get(`/api/accounts/${id}`), boundApi.get(`/api/accounts/relationships?id=${id}`)]).then(values => { + dispatch(fetchAccountSuccess(values[0].data, values[1].data[0])); }).catch(error => { dispatch(fetchAccountFail(id, error)); }); @@ -49,6 +56,20 @@ export function fetchAccountTimeline(id) { }; }; +export function expandAccountTimeline(id) { + return (dispatch, getState) => { + const lastId = getState().getIn(['timelines', 'accounts_timelines', id]).last(); + + dispatch(expandAccountTimelineRequest(id)); + + api(getState).get(`/api/accounts/${id}/statuses?max_id=${lastId}`).then(response => { + dispatch(expandAccountTimelineSuccess(id, response.data)); + }).catch(error => { + dispatch(expandAccountTimelineFail(id, error)); + }); + }; +}; + export function fetchAccountRequest(id) { return { type: ACCOUNT_FETCH_REQUEST, @@ -56,10 +77,11 @@ export function fetchAccountRequest(id) { }; }; -export function fetchAccountSuccess(account) { +export function fetchAccountSuccess(account, relationship) { return { type: ACCOUNT_FETCH_SUCCESS, - account: account + account: account, + relationship: relationship }; }; @@ -159,3 +181,26 @@ export function fetchAccountTimelineFail(id, error) { error: error }; }; + +export function expandAccountTimelineRequest(id) { + return { + type: ACCOUNT_TIMELINE_EXPAND_REQUEST, + id: id + }; +}; + +export function expandAccountTimelineSuccess(id, statuses) { + return { + type: ACCOUNT_TIMELINE_EXPAND_SUCCESS, + id: id, + statuses: statuses + }; +}; + +export function expandAccountTimelineFail(id, error) { + return { + type: ACCOUNT_TIMELINE_EXPAND_FAIL, + id: id, + error: error + }; +}; diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx index edbc34826..46d0e5954 100644 --- a/app/assets/javascripts/components/features/account/index.jsx +++ b/app/assets/javascripts/components/features/account/index.jsx @@ -1,13 +1,19 @@ -import { connect } from 'react-redux'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchAccount, followAccount, unfollowAccount, fetchAccountTimeline } from '../../actions/accounts'; -import { replyCompose } from '../../actions/compose'; -import { favourite, reblog } from '../../actions/interactions'; -import Header from './components/header'; -import { selectStatus } from '../../reducers/timelines'; -import StatusList from '../../components/status_list'; -import Immutable from 'immutable'; +import { connect } from 'react-redux'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { + fetchAccount, + followAccount, + unfollowAccount, + fetchAccountTimeline, + expandAccountTimeline +} from '../../actions/accounts'; +import { replyCompose } from '../../actions/compose'; +import { favourite, reblog } from '../../actions/interactions'; +import Header from './components/header'; +import { selectStatus } from '../../reducers/timelines'; +import StatusList from '../../components/status_list'; +import Immutable from 'immutable'; function selectAccount(state, id) { return state.getIn(['timelines', 'accounts', id], null); @@ -65,6 +71,10 @@ const Account = React.createClass({ this.props.dispatch(favourite(status)); }, + handleScrollToBottom () { + this.props.dispatch(expandAccountTimeline(this.props.account.get('id'))); + }, + render () { const { account, statuses } = this.props; @@ -75,7 +85,7 @@ const Account = React.createClass({ return (
- +
); } diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx index 47641557d..995947b2d 100644 --- a/app/assets/javascripts/components/reducers/notifications.jsx +++ b/app/assets/javascripts/components/reducers/notifications.jsx @@ -10,7 +10,8 @@ import { ACCOUNT_FETCH_FAIL, ACCOUNT_FOLLOW_FAIL, ACCOUNT_UNFOLLOW_FAIL, - ACCOUNT_TIMELINE_FETCH_FAIL + ACCOUNT_TIMELINE_FETCH_FAIL, + ACCOUNT_TIMELINE_EXPAND_FAIL } from '../actions/accounts'; import { STATUS_FETCH_FAIL } from '../actions/statuses'; import Immutable from 'immutable'; @@ -48,6 +49,7 @@ export default function notifications(state = initialState, action) { case ACCOUNT_FOLLOW_FAIL: case ACCOUNT_UNFOLLOW_FAIL: case ACCOUNT_TIMELINE_FETCH_FAIL: + case ACCOUNT_TIMELINE_EXPAND_FAIL: case STATUS_FETCH_FAIL: return notificationFromError(state, action.error); case NOTIFICATION_DISMISS: diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index e3de9e9b2..8dd9c3b1f 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -13,7 +13,8 @@ import { ACCOUNT_FETCH_SUCCESS, ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS, - ACCOUNT_TIMELINE_FETCH_SUCCESS + ACCOUNT_TIMELINE_FETCH_SUCCESS, + ACCOUNT_TIMELINE_EXPAND_SUCCESS } from '../actions/accounts'; import { STATUS_FETCH_SUCCESS } from '../actions/statuses'; import { FOLLOW_SUBMIT_SUCCESS } from '../actions/follow'; @@ -110,6 +111,17 @@ function normalizeAccountTimeline(state, accountId, statuses) { return state; }; +function appendNormalizedAccountTimeline(state, accountId, statuses) { + let moreIds = Immutable.List(); + + statuses.forEach((status, i) => { + state = normalizeStatus(state, status); + moreIds = moreIds.set(i, status.get('id')); + }); + + return state.updateIn(['accounts_timelines', accountId], Immutable.List(), list => list.push(...moreIds)); +}; + function updateTimeline(state, timeline, status) { state = normalizeStatus(state, status); state = state.update(timeline, list => list.unshift(status.get('id'))); @@ -176,6 +188,8 @@ export default function timelines(state = initialState, action) { return normalizeContext(state, Immutable.fromJS(action.status), Immutable.fromJS(action.context.ancestors), Immutable.fromJS(action.context.descendants)); case ACCOUNT_TIMELINE_FETCH_SUCCESS: return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); + case ACCOUNT_TIMELINE_EXPAND_SUCCESS: + return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); default: return state; }