Refactoring redux state into different reducers

This commit is contained in:
Eugen Rochko 2016-10-30 15:06:43 +01:00
parent 7060bdf04b
commit e8ff4c8e56
23 changed files with 352 additions and 223 deletions

View file

@ -1,5 +1,4 @@
import api from '../api' import api from '../api'
import axios from 'axios';
import Immutable from 'immutable'; import Immutable from 'immutable';
export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF'; export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF';
@ -53,12 +52,11 @@ export function setAccountSelf(account) {
export function fetchAccount(id) { export function fetchAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const boundApi = api(getState);
dispatch(fetchAccountRequest(id)); dispatch(fetchAccountRequest(id));
axios.all([boundApi.get(`/api/v1/accounts/${id}`), boundApi.get(`/api/v1/accounts/relationships?id=${id}`)]).then(values => { api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(fetchAccountSuccess(values[0].data, values[1].data[0])); dispatch(fetchAccountSuccess(response.data));
dispatch(fetchRelationships([id]));
}).catch(error => { }).catch(error => {
dispatch(fetchAccountFail(id, error)); dispatch(fetchAccountFail(id, error));
}); });
@ -107,11 +105,10 @@ export function fetchAccountRequest(id) {
}; };
}; };
export function fetchAccountSuccess(account, relationship) { export function fetchAccountSuccess(account) {
return { return {
type: ACCOUNT_FETCH_SUCCESS, type: ACCOUNT_FETCH_SUCCESS,
account: account, account: account
relationship: relationship
}; };
}; };

View file

@ -1,5 +1,6 @@
import api from '../api'; import api from '../api';
import axios from 'axios';
import { deleteFromTimelines } from './timelines';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@ -9,6 +10,10 @@ export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
export function fetchStatusRequest(id) { export function fetchStatusRequest(id) {
return { return {
type: STATUS_FETCH_REQUEST, type: STATUS_FETCH_REQUEST,
@ -18,12 +23,11 @@ export function fetchStatusRequest(id) {
export function fetchStatus(id) { export function fetchStatus(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const boundApi = api(getState);
dispatch(fetchStatusRequest(id)); dispatch(fetchStatusRequest(id));
axios.all([boundApi.get(`/api/v1/statuses/${id}`), boundApi.get(`/api/v1/statuses/${id}/context`)]).then(values => { api(getState).get(`/api/v1/statuses/${id}`).then(response => {
dispatch(fetchStatusSuccess(values[0].data, values[1].data)); dispatch(fetchStatusSuccess(response.data));
dispatch(fetchContext(id));
}).catch(error => { }).catch(error => {
dispatch(fetchStatusFail(id, error)); dispatch(fetchStatusFail(id, error));
}); });
@ -52,6 +56,7 @@ export function deleteStatus(id) {
api(getState).delete(`/api/v1/statuses/${id}`).then(response => { api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));
}); });
@ -79,3 +84,40 @@ export function deleteStatusFail(id, error) {
error: error error: error
}; };
}; };
export function fetchContext(id) {
return (dispatch, getState) => {
dispatch(fetchContextRequest(id));
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
}).catch(error => {
dispatch(fetchContextFail(id, error));
});
};
};
export function fetchContextRequest(id) {
return {
type: CONTEXT_FETCH_REQUEST,
id
};
};
export function fetchContextSuccess(id, ancestors, descendants) {
return {
type: CONTEXT_FETCH_SUCCESS,
id,
ancestors,
descendants,
statuses: ancestors.concat(descendants)
};
};
export function fetchContextFail(id, error) {
return {
type: CONTEXT_FETCH_FAIL,
id,
error
};
};

View file

@ -21,17 +21,29 @@ export function refreshTimelineSuccess(timeline, statuses, replace) {
}; };
export function updateTimeline(timeline, status) { export function updateTimeline(timeline, status) {
return { return (dispatch, getState) => {
const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];
dispatch({
type: TIMELINE_UPDATE, type: TIMELINE_UPDATE,
timeline: timeline, timeline,
status: status status,
references
});
}; };
}; };
export function deleteFromTimelines(id) { export function deleteFromTimelines(id) {
return { return (dispatch, getState) => {
const accountId = getState().getIn(['statuses', id, 'account']);
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
dispatch({
type: TIMELINE_DELETE, type: TIMELINE_DELETE,
id: id id,
accountId,
references
});
}; };
}; };

View file

@ -15,7 +15,7 @@ const Status = React.createClass({
}, },
propTypes: { propTypes: {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map,
wrapped: React.PropTypes.bool, wrapped: React.PropTypes.bool,
onReply: React.PropTypes.func, onReply: React.PropTypes.func,
onFavourite: React.PropTypes.func, onFavourite: React.PropTypes.func,

View file

@ -19,7 +19,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id), status: getStatus(state, props.id),
me: state.getIn(['timelines', 'me']) me: state.getIn(['meta', 'me'])
}); });
return mapStateToProps; return mapStateToProps;

View file

@ -26,7 +26,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
account: getAccount(state, Number(props.params.accountId)), account: getAccount(state, Number(props.params.accountId)),
me: state.getIn(['timelines', 'me']) me: state.getIn(['meta', 'me'])
}); });
return mapStateToProps; return mapStateToProps;

View file

@ -10,7 +10,7 @@ import LoadingIndicator from '../../components/loading_indicator';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId)]), statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId)]),
me: state.getIn(['timelines', 'me']) me: state.getIn(['meta', 'me'])
}); });
const AccountTimeline = React.createClass({ const AccountTimeline = React.createClass({

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import SuggestionsBox from '../components/suggestions_box'; import SuggestionsBox from '../components/suggestions_box';
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
accountIds: state.get('suggestions') accountIds: state.getIn(['user_lists', 'suggestions'])
}); });
export default connect(mapStateToProps)(SuggestionsBox); export default connect(mapStateToProps)(SuggestionsBox);

View file

@ -11,7 +11,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id), account: getAccount(state, props.id),
me: state.getIn(['timelines', 'me']) me: state.getIn(['meta', 'me'])
}); });
return mapStateToProps; return mapStateToProps;

View file

@ -31,7 +31,7 @@ const makeMapStateToProps = () => {
status: getStatus(state, Number(props.params.statusId)), status: getStatus(state, Number(props.params.statusId)),
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]), ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]), descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
me: state.getIn(['timelines', 'me']) me: state.getIn(['meta', 'me'])
}); });
return mapStateToProps; return mapStateToProps;
@ -43,8 +43,8 @@ const Status = React.createClass({
params: React.PropTypes.object.isRequired, params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired, dispatch: React.PropTypes.func.isRequired,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.orderedSet, ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.orderedSet descendantsIds: ImmutablePropTypes.list
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
@ -101,11 +101,11 @@ const Status = React.createClass({
const account = status.get('account'); const account = status.get('account');
if (ancestorsIds) { if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds)}</div>; ancestors = <div>{this.renderChildren(ancestorsIds)}</div>;
} }
if (descendantsIds) { if (descendantsIds && descendantsIds.size > 0) {
descendants = <div>{this.renderChildren(descendantsIds)}</div>; descendants = <div>{this.renderChildren(descendantsIds)}</div>;
} }

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import NavigationBar from '../components/navigation_bar'; import NavigationBar from '../components/navigation_bar';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
account: state.getIn(['timelines', 'accounts', state.getIn(['timelines', 'me'])]) account: state.getIn(['accounts', state.getIn(['meta', 'me'])])
}); });
export default connect(mapStateToProps)(NavigationBar); export default connect(mapStateToProps)(NavigationBar);

View file

@ -0,0 +1,80 @@
import {
ACCOUNT_SET_SELF,
ACCOUNT_FETCH_SUCCESS,
FOLLOWERS_FETCH_SUCCESS,
FOLLOWING_FETCH_SUCCESS,
ACCOUNT_TIMELINE_FETCH_SUCCESS,
ACCOUNT_TIMELINE_EXPAND_SUCCESS
} from '../actions/accounts';
import { FOLLOW_SUBMIT_SUCCESS } from '../actions/follow';
import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions';
import {
REBLOG_SUCCESS,
UNREBLOG_SUCCESS,
FAVOURITE_SUCCESS,
UNFAVOURITE_SUCCESS
} from '../actions/interactions';
import {
TIMELINE_REFRESH_SUCCESS,
TIMELINE_UPDATE,
TIMELINE_EXPAND_SUCCESS
} from '../actions/timelines';
import { STATUS_FETCH_SUCCESS } from '../actions/statuses';
import Immutable from 'immutable';
const normalizeAccount = (state, account) => state.set(account.get('id'), account);
const normalizeAccounts = (state, accounts) => {
accounts.forEach(account => {
state = normalizeAccount(state, account);
});
return state;
};
const normalizeAccountFromStatus = (state, status) => {
state = normalizeAccount(state, status.get('account'));
if (status.getIn(['reblog', 'account'])) {
state = normalizeAccount(state, status.getIn(['reblog', 'account']));
}
return state;
};
const normalizeAccountsFromStatuses = (state, statuses) => {
statuses.forEach(status => {
state = normalizeAccountFromStatus(state, status);
});
return state;
};
const initialState = Immutable.Map();
export default function accounts(state = initialState, action) {
switch(action.type) {
case ACCOUNT_SET_SELF:
case ACCOUNT_FETCH_SUCCESS:
case FOLLOW_SUBMIT_SUCCESS:
return normalizeAccount(state, Immutable.fromJS(action.account));
case SUGGESTIONS_FETCH_SUCCESS:
case FOLLOWERS_FETCH_SUCCESS:
case FOLLOWING_FETCH_SUCCESS:
return normalizeAccounts(state, Immutable.fromJS(action.accounts));
case TIMELINE_REFRESH_SUCCESS:
case TIMELINE_EXPAND_SUCCESS:
case ACCOUNT_TIMELINE_FETCH_SUCCESS:
case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
return normalizeAccountsFromStatuses(state, Immutable.fromJS(action.statuses));
case TIMELINE_UPDATE:
case REBLOG_SUCCESS:
case FAVOURITE_SUCCESS:
case UNREBLOG_SUCCESS:
case UNFAVOURITE_SUCCESS:
case STATUS_FETCH_SUCCESS:
return normalizeAccountFromStatus(state, Immutable.fromJS(action.status));
default:
return state;
}
};

View file

@ -7,7 +7,9 @@ import notifications from './notifications';
import { loadingBarReducer } from 'react-redux-loading-bar'; import { loadingBarReducer } from 'react-redux-loading-bar';
import modal from './modal'; import modal from './modal';
import user_lists from './user_lists'; import user_lists from './user_lists';
import suggestions from './suggestions'; import accounts from './accounts';
import statuses from './statuses';
import relationships from './relationships';
export default combineReducers({ export default combineReducers({
timelines, timelines,
@ -18,5 +20,7 @@ export default combineReducers({
loadingBar: loadingBarReducer, loadingBar: loadingBarReducer,
modal, modal,
user_lists, user_lists,
suggestions accounts,
statuses,
relationships
}); });

View file

@ -1,4 +1,5 @@
import { ACCESS_TOKEN_SET } from '../actions/meta'; import { ACCESS_TOKEN_SET } from '../actions/meta';
import { ACCOUNT_SET_SELF } from '../actions/accounts';
import Immutable from 'immutable'; import Immutable from 'immutable';
const initialState = Immutable.Map(); const initialState = Immutable.Map();
@ -7,6 +8,8 @@ export default function meta(state = initialState, action) {
switch(action.type) { switch(action.type) {
case ACCESS_TOKEN_SET: case ACCESS_TOKEN_SET:
return state.set('access_token', action.token); return state.set('access_token', action.token);
case ACCOUNT_SET_SELF:
return state.set('me', action.account.id);
default: default:
return state; return state;
} }

View file

@ -0,0 +1,34 @@
import {
ACCOUNT_FOLLOW_SUCCESS,
ACCOUNT_UNFOLLOW_SUCCESS,
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_UNBLOCK_SUCCESS,
RELATIONSHIPS_FETCH_SUCCESS
} from '../actions/accounts';
import Immutable from 'immutable';
const normalizeRelationship = (state, relationship) => state.set(relationship.get('id'), relationship);
const normalizeRelationships = (state, relationships) => {
relationships.forEach(relationship => {
state = normalizeRelationship(state, relationship);
});
return state;
};
const initialState = Immutable.Map();
export default function relationships(state = initialState, action) {
switch(action.type) {
case ACCOUNT_FOLLOW_SUCCESS:
case ACCOUNT_UNFOLLOW_SUCCESS:
case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_UNBLOCK_SUCCESS:
return normalizeRelationship(state, Immutable.fromJS(action.relationship));
case RELATIONSHIPS_FETCH_SUCCESS:
return normalizeRelationships(state, Immutable.fromJS(action.relationships));
default:
return state;
}
};

View file

@ -0,0 +1,68 @@
import {
REBLOG_SUCCESS,
UNREBLOG_SUCCESS,
FAVOURITE_SUCCESS,
UNFAVOURITE_SUCCESS
} from '../actions/interactions';
import {
STATUS_FETCH_SUCCESS,
CONTEXT_FETCH_SUCCESS
} from '../actions/statuses';
import {
TIMELINE_REFRESH_SUCCESS,
TIMELINE_UPDATE,
TIMELINE_DELETE,
TIMELINE_EXPAND_SUCCESS
} from '../actions/timelines';
import {
ACCOUNT_TIMELINE_FETCH_SUCCESS,
ACCOUNT_TIMELINE_EXPAND_SUCCESS
} from '../actions/accounts';
import Immutable from 'immutable';
const normalizeStatus = (state, status) => {
status = status.set('account', status.getIn(['account', 'id']));
if (status.getIn(['reblog', 'id'])) {
state = normalizeStatus(state, status.get('reblog'));
status = status.set('reblog', status.getIn(['reblog', 'id']));
}
return state.set(status.get('id'), status);
};
const normalizeStatuses = (state, statuses) => {
statuses.forEach(status => {
state = normalizeStatus(state, status);
});
return state;
};
const deleteStatus = (state, id, references) => {
references.forEach(ref => {
state = deleteStatus(state, ref[0], []);
});
return state.delete(id);
};
const initialState = Immutable.Map();
export default function statuses(state = initialState, action) {
switch(action.type) {
case TIMELINE_UPDATE:
case STATUS_FETCH_SUCCESS:
return normalizeStatus(state, Immutable.fromJS(action.status));
case TIMELINE_REFRESH_SUCCESS:
case TIMELINE_EXPAND_SUCCESS:
case ACCOUNT_TIMELINE_FETCH_SUCCESS:
case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
case CONTEXT_FETCH_SUCCESS:
return normalizeStatuses(state, Immutable.fromJS(action.statuses));
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
default:
return state;
}
};

View file

@ -1,13 +0,0 @@
import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions';
import Immutable from 'immutable';
const initialState = Immutable.List();
export default function suggestions(state = initialState, action) {
switch(action.type) {
case SUGGESTIONS_FETCH_SUCCESS:
return Immutable.List(action.accounts.map(item => item.id));
default:
return state;
}
}

View file

@ -11,77 +11,44 @@ import {
UNFAVOURITE_SUCCESS UNFAVOURITE_SUCCESS
} from '../actions/interactions'; } from '../actions/interactions';
import { import {
ACCOUNT_SET_SELF,
ACCOUNT_FETCH_SUCCESS, ACCOUNT_FETCH_SUCCESS,
ACCOUNT_FOLLOW_SUCCESS,
ACCOUNT_UNFOLLOW_SUCCESS,
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_UNBLOCK_SUCCESS,
ACCOUNT_TIMELINE_FETCH_SUCCESS, ACCOUNT_TIMELINE_FETCH_SUCCESS,
ACCOUNT_TIMELINE_EXPAND_SUCCESS, ACCOUNT_TIMELINE_EXPAND_SUCCESS
FOLLOWERS_FETCH_SUCCESS,
FOLLOWING_FETCH_SUCCESS,
RELATIONSHIPS_FETCH_SUCCESS
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
STATUS_FETCH_SUCCESS, STATUS_FETCH_SUCCESS,
STATUS_DELETE_SUCCESS CONTEXT_FETCH_SUCCESS
} from '../actions/statuses'; } from '../actions/statuses';
import { FOLLOW_SUBMIT_SUCCESS } from '../actions/follow';
import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions';
import Immutable from 'immutable'; import Immutable from 'immutable';
const initialState = Immutable.Map({ const initialState = Immutable.Map({
home: Immutable.List([]), home: Immutable.List(),
mentions: Immutable.List([]), mentions: Immutable.List(),
public: Immutable.List([]), public: Immutable.List(),
statuses: Immutable.Map(),
accounts: Immutable.Map(),
accounts_timelines: Immutable.Map(), accounts_timelines: Immutable.Map(),
me: null,
ancestors: Immutable.Map(), ancestors: Immutable.Map(),
descendants: Immutable.Map(), descendants: Immutable.Map()
relationships: Immutable.Map(),
suggestions: Immutable.List([])
}); });
function normalizeStatus(state, status) { const normalizeStatus = (state, status) => {
// Separate account const replyToId = status.get('in_reply_to_id');
let account = status.get('account'); const id = status.get('id');
status = status.set('account', account.get('id'));
// Separate reblog, repeat for reblog if (replyToId) {
let reblog = status.get('reblog', null); if (!state.getIn(['descendants', replyToId], Immutable.List()).includes(id)) {
state = state.updateIn(['descendants', replyToId], Immutable.List(), set => set.push(id));
if (reblog !== null) {
status = status.set('reblog', reblog.get('id'));
state = normalizeStatus(state, reblog);
} }
// Replies if (!state.getIn(['ancestors', id], Immutable.List()).includes(replyToId)) {
if (status.get('in_reply_to_id')) { state = state.updateIn(['ancestors', id], Immutable.List(), set => set.push(replyToId));
state = state.updateIn(['descendants', status.get('in_reply_to_id')], set => {
if (!Immutable.OrderedSet.isOrderedSet(set)) {
return Immutable.OrderedSet([status.get('id')]);
} else {
return set.add(status.get('id'));
} }
});
} }
return state.withMutations(map => { return state;
if (status.get('in_reply_to_id')) {
map.updateIn(['descendants', status.get('in_reply_to_id')], Immutable.OrderedSet(), set => set.add(status.get('id')));
map.updateIn(['ancestors', status.get('id')], Immutable.OrderedSet(), set => set.add(status.get('in_reply_to_id')));
}
map.setIn(['accounts', account.get('id')], account);
map.setIn(['statuses', status.get('id')], status);
});
}; };
function normalizeTimeline(state, timeline, statuses, replace = false) { const normalizeTimeline = (state, timeline, statuses, replace = false) => {
let ids = Immutable.List([]); let ids = Immutable.List();
statuses.forEach((status, i) => { statuses.forEach((status, i) => {
state = normalizeStatus(state, status); state = normalizeStatus(state, status);
@ -91,8 +58,8 @@ function normalizeTimeline(state, timeline, statuses, replace = false) {
return state.update(timeline, list => (replace ? ids : list.unshift(...ids))); return state.update(timeline, list => (replace ? ids : list.unshift(...ids)));
}; };
function appendNormalizedTimeline(state, timeline, statuses) { const appendNormalizedTimeline = (state, timeline, statuses) => {
let moreIds = Immutable.List([]); let moreIds = Immutable.List();
statuses.forEach((status, i) => { statuses.forEach((status, i) => {
state = normalizeStatus(state, status); state = normalizeStatus(state, status);
@ -102,8 +69,8 @@ function appendNormalizedTimeline(state, timeline, statuses) {
return state.update(timeline, list => list.push(...moreIds)); return state.update(timeline, list => list.push(...moreIds));
}; };
function normalizeAccountTimeline(state, accountId, statuses, replace = false) { const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => {
let ids = Immutable.List([]); let ids = Immutable.List();
statuses.forEach((status, i) => { statuses.forEach((status, i) => {
state = normalizeStatus(state, status); state = normalizeStatus(state, status);
@ -113,7 +80,7 @@ function normalizeAccountTimeline(state, accountId, statuses, replace = false) {
return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => (replace ? ids : list.unshift(...ids))); return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => (replace ? ids : list.unshift(...ids)));
}; };
function appendNormalizedAccountTimeline(state, accountId, statuses) { const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
let moreIds = Immutable.List([]); let moreIds = Immutable.List([]);
statuses.forEach((status, i) => { statuses.forEach((status, i) => {
@ -124,107 +91,60 @@ function appendNormalizedAccountTimeline(state, accountId, statuses) {
return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.push(...moreIds)); return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.push(...moreIds));
}; };
function updateTimeline(state, timeline, status) { const updateTimeline = (state, timeline, status, references) => {
state = normalizeStatus(state, status); state = normalizeStatus(state, status);
state = state.update(timeline, list => { state = state.update(timeline, list => {
const reblogOfId = status.getIn(['reblog', 'id'], null); const reblogOfId = status.getIn(['reblog', 'id'], null);
if (reblogOfId !== null) { if (reblogOfId !== null) {
const otherReblogs = state.get('statuses').filter(item => item.get('reblog') === reblogOfId).map((_, itemId) => itemId); list = list.filterNot(itemId => references.includes(itemId));
list = list.filterNot(itemId => (itemId === reblogOfId || otherReblogs.includes(itemId)));
} }
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'))));
return state; return state;
}; };
function deleteStatus(state, id) { const deleteStatus = (state, id, accountId, references) => {
const status = state.getIn(['statuses', id]);
if (!status) {
return state;
}
// Remove references from timelines // Remove references from timelines
['home', 'mentions'].forEach(function (timeline) { ['home', 'mentions', 'public'].forEach(function (timeline) {
state = state.update(timeline, list => list.filterNot(item => item === id)); state = state.update(timeline, list => list.filterNot(item => item === id));
}); });
// Remove references from account timelines // Remove references from account timelines
state = state.updateIn(['accounts_timelines', status.get('account')], Immutable.List([]), list => list.filterNot(item => item === id)); state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.filterNot(item => item === id));
// Remove references from context
state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => {
state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
});
state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => {
state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
});
state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]);
// Remove reblogs of deleted status // Remove reblogs of deleted status
const references = state.get('statuses').filter(item => item.get('reblog') === id); references.forEach(ref => {
state = deleteStatus(state, ref[0], ref[1], []);
references.forEach(referencingId => {
state = deleteStatus(state, referencingId);
});
// Remove normalized status
return state.deleteIn(['statuses', id]);
};
function normalizeAccount(state, account, relationship) {
if (relationship) {
state = normalizeRelationship(state, relationship);
}
return state.setIn(['accounts', account.get('id')], account);
};
function normalizeRelationship(state, relationship) {
if (state.get('suggestions').includes(relationship.get('id')) && (relationship.get('following') || relationship.get('blocking'))) {
state = state.update('suggestions', list => list.filterNot(id => id === relationship.get('id')));
}
return state.setIn(['relationships', relationship.get('id')], relationship);
};
function normalizeRelationships(state, relationships) {
relationships.forEach(relationship => {
state = normalizeRelationship(state, relationship);
}); });
return state; return state;
}; };
function setSelf(state, account) { const normalizeContext = (state, id, ancestors, descendants) => {
state = normalizeAccount(state, account); const ancestorsIds = ancestors.map(ancestor => ancestor.get('id'));
return state.set('me', account.get('id')); const descendantsIds = descendants.map(descendant => descendant.get('id'));
};
function normalizeContext(state, status, ancestors, descendants) {
state = normalizeStatus(state, status);
let ancestorsIds = ancestors.map(ancestor => {
state = normalizeStatus(state, ancestor);
return ancestor.get('id');
}).toOrderedSet();
let descendantsIds = descendants.map(descendant => {
state = normalizeStatus(state, descendant);
return descendant.get('id');
}).toOrderedSet();
return state.withMutations(map => { return state.withMutations(map => {
map.setIn(['ancestors', status.get('id')], ancestorsIds); map.setIn(['ancestors', id], ancestorsIds);
map.setIn(['descendants', status.get('id')], descendantsIds); map.setIn(['descendants', id], descendantsIds);
}); });
}; };
function normalizeAccounts(state, accounts) {
accounts.forEach(account => {
state = state.setIn(['accounts', account.get('id')], account);
});
return state;
};
export default function timelines(state = initialState, action) { export default function timelines(state = initialState, action) {
switch(action.type) { switch(action.type) {
case TIMELINE_REFRESH_SUCCESS: case TIMELINE_REFRESH_SUCCESS:
@ -232,37 +152,15 @@ export default function timelines(state = initialState, action) {
case TIMELINE_EXPAND_SUCCESS: case TIMELINE_EXPAND_SUCCESS:
return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
case TIMELINE_UPDATE: case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, Immutable.fromJS(action.status)); return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
case TIMELINE_DELETE: case TIMELINE_DELETE:
case STATUS_DELETE_SUCCESS: return deleteStatus(state, action.id, action.accountId, action.references);
return deleteStatus(state, action.id); case CONTEXT_FETCH_SUCCESS:
case REBLOG_SUCCESS: return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants));
case FAVOURITE_SUCCESS:
case UNREBLOG_SUCCESS:
case UNFAVOURITE_SUCCESS:
return normalizeStatus(state, Immutable.fromJS(action.response));
case ACCOUNT_SET_SELF:
return setSelf(state, Immutable.fromJS(action.account));
case ACCOUNT_FETCH_SUCCESS:
case FOLLOW_SUBMIT_SUCCESS:
return normalizeAccount(state, Immutable.fromJS(action.account), Immutable.fromJS(action.relationship));
case ACCOUNT_FOLLOW_SUCCESS:
case ACCOUNT_UNFOLLOW_SUCCESS:
case ACCOUNT_UNBLOCK_SUCCESS:
case ACCOUNT_BLOCK_SUCCESS:
return normalizeRelationship(state, Immutable.fromJS(action.relationship));
case STATUS_FETCH_SUCCESS:
return normalizeContext(state, Immutable.fromJS(action.status), Immutable.fromJS(action.context.ancestors), Immutable.fromJS(action.context.descendants));
case ACCOUNT_TIMELINE_FETCH_SUCCESS: case ACCOUNT_TIMELINE_FETCH_SUCCESS:
return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace); return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace);
case ACCOUNT_TIMELINE_EXPAND_SUCCESS: case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
case SUGGESTIONS_FETCH_SUCCESS:
case FOLLOWERS_FETCH_SUCCESS:
case FOLLOWING_FETCH_SUCCESS:
return normalizeAccounts(state, Immutable.fromJS(action.accounts));
case RELATIONSHIPS_FETCH_SUCCESS:
return normalizeRelationships(state, Immutable.fromJS(action.relationships));
default: default:
return state; return state;
} }

View file

@ -2,11 +2,13 @@ import {
FOLLOWERS_FETCH_SUCCESS, FOLLOWERS_FETCH_SUCCESS,
FOLLOWING_FETCH_SUCCESS FOLLOWING_FETCH_SUCCESS
} from '../actions/accounts'; } from '../actions/accounts';
import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions';
import Immutable from 'immutable'; import Immutable from 'immutable';
const initialState = Immutable.Map({ const initialState = Immutable.Map({
followers: Immutable.Map(), followers: Immutable.Map(),
following: Immutable.Map() following: Immutable.Map(),
suggestions: Immutable.List()
}); });
export default function userLists(state = initialState, action) { export default function userLists(state = initialState, action) {
@ -15,6 +17,8 @@ export default function userLists(state = initialState, action) {
return state.setIn(['followers', action.id], Immutable.List(action.accounts.map(item => item.id))); return state.setIn(['followers', action.id], Immutable.List(action.accounts.map(item => item.id)));
case FOLLOWING_FETCH_SUCCESS: case FOLLOWING_FETCH_SUCCESS:
return state.setIn(['following', action.id], Immutable.List(action.accounts.map(item => item.id))); return state.setIn(['following', action.id], Immutable.List(action.accounts.map(item => item.id)));
case SUGGESTIONS_FETCH_SUCCESS:
return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id)));
default: default:
return state; return state;
} }

View file

@ -1,11 +1,11 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import Immutable from 'immutable'; import Immutable from 'immutable';
const getStatuses = state => state.getIn(['timelines', 'statuses']); const getStatuses = state => state.get('statuses');
const getAccounts = state => state.getIn(['timelines', 'accounts']); const getAccounts = state => state.get('accounts');
const getAccountBase = (state, id) => state.getIn(['timelines', 'accounts', id], null); const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
const getAccountRelationship = (state, id) => state.getIn(['timelines', 'relationships', id]); const getAccountRelationship = (state, id) => state.getIn(['relationships', id]);
export const makeGetAccount = () => { export const makeGetAccount = () => {
return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => { return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => {
@ -17,7 +17,7 @@ export const makeGetAccount = () => {
}); });
}; };
const getStatusBase = (state, id) => state.getIn(['timelines', 'statuses', id], null); const getStatusBase = (state, id) => state.getIn(['statuses', id], null);
export const makeGetStatus = () => { export const makeGetStatus = () => {
return createSelector([getStatusBase, getStatuses, getAccounts], (base, statuses, accounts) => { return createSelector([getStatusBase, getStatuses, getAccounts], (base, statuses, accounts) => {