forked from cybrespace/mastodon
Use reselect to memoize denormalization in UI state
Also upgrade react-redux to latest version. This is a performance update
This commit is contained in:
parent
1f650d327d
commit
ef9d4f4e06
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
window.React = require('react');
|
window.React = require('react');
|
||||||
window.ReactDOM = require('react-dom');
|
window.ReactDOM = require('react-dom');
|
||||||
|
window.Perf = require('react-addons-perf');
|
||||||
|
|
||||||
//= require_tree ./components
|
//= require_tree ./components
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ const StatusList = React.createClass({
|
||||||
const { statuses, onScrollToBottom, ...other } = this.props;
|
const { statuses, onScrollToBottom, ...other } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ overflowY: 'scroll', flex: '1 1 auto' }} 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) => {
|
||||||
return <Status key={status.get('id')} {...other} status={status} />;
|
return <Status key={status.get('id')} {...other} status={status} />;
|
||||||
|
|
|
@ -20,22 +20,18 @@ import {
|
||||||
} from '../../actions/interactions';
|
} from '../../actions/interactions';
|
||||||
import Header from './components/header';
|
import Header from './components/header';
|
||||||
import {
|
import {
|
||||||
selectStatus,
|
getAccountTimeline,
|
||||||
selectAccount
|
getAccount
|
||||||
} from '../../reducers/timelines';
|
} from '../../selectors';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import ActionBar from './components/action_bar';
|
import ActionBar from './components/action_bar';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
function selectStatuses(state, accountId) {
|
|
||||||
return state.getIn(['timelines', 'accounts_timelines', accountId], Immutable.List([])).map(id => selectStatus(state, id)).filterNot(status => status === null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
account: selectAccount(state, Number(props.params.accountId)),
|
account: getAccount(state, Number(props.params.accountId)),
|
||||||
statuses: selectStatuses(state, Number(props.params.accountId)),
|
statuses: getAccountTimeline(state, Number(props.params.accountId)),
|
||||||
me: state.getIn(['timelines', 'me'])
|
me: state.getIn(['timelines', 'me'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,35 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import { selectStatus } from '../../reducers/timelines';
|
import { makeGetTimeline } from '../../selectors';
|
||||||
import {
|
import {
|
||||||
updateTimeline,
|
updateTimeline,
|
||||||
refreshTimeline,
|
refreshTimeline,
|
||||||
expandTimeline
|
expandTimeline
|
||||||
} from '../../actions/timelines';
|
} from '../../actions/timelines';
|
||||||
import { deleteStatus } from '../../actions/statuses';
|
import { deleteStatus } from '../../actions/statuses';
|
||||||
import { replyCompose } from '../../actions/compose';
|
import { replyCompose } from '../../actions/compose';
|
||||||
import {
|
import {
|
||||||
favourite,
|
favourite,
|
||||||
reblog,
|
reblog,
|
||||||
unreblog,
|
unreblog,
|
||||||
unfavourite
|
unfavourite
|
||||||
} from '../../actions/interactions';
|
} from '../../actions/interactions';
|
||||||
|
|
||||||
function selectStatuses(state) {
|
const makeMapStateToProps = () => {
|
||||||
return state.getIn(['timelines', 'public'], Immutable.List()).map(id => selectStatus(state, id)).filterNot(status => status === null);
|
const getTimeline = makeGetTimeline();
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
statuses: getTimeline(state, 'public'),
|
||||||
|
me: state.getIn(['timelines', 'me'])
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
|
||||||
statuses: selectStatuses(state),
|
|
||||||
me: state.getIn(['timelines', 'me'])
|
|
||||||
});
|
|
||||||
|
|
||||||
const PublicTimeline = React.createClass({
|
const PublicTimeline = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -100,4 +102,4 @@ const PublicTimeline = React.createClass({
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(PublicTimeline);
|
export default connect(makeMapStateToProps)(PublicTimeline);
|
||||||
|
|
|
@ -10,16 +10,16 @@ import ActionBar from './components/action_bar';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import { favourite, reblog } from '../../actions/interactions';
|
import { favourite, reblog } from '../../actions/interactions';
|
||||||
import { replyCompose } from '../../actions/compose';
|
import { replyCompose } from '../../actions/compose';
|
||||||
import { selectStatus } from '../../reducers/timelines';
|
import {
|
||||||
|
getStatus,
|
||||||
function selectStatuses(state, ids) {
|
getStatusAncestors,
|
||||||
return ids.map(id => selectStatus(state, id)).filterNot(status => status === null);
|
getStatusDescendants
|
||||||
};
|
} from '../../selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
status: selectStatus(state, Number(props.params.statusId)),
|
status: getStatus(state, Number(props.params.statusId)),
|
||||||
ancestors: selectStatuses(state, state.getIn(['timelines', 'ancestors', Number(props.params.statusId)], Immutable.OrderedSet())),
|
ancestors: getStatusAncestors(state, Number(props.params.statusId)),
|
||||||
descendants: selectStatuses(state, state.getIn(['timelines', 'descendants', Number(props.params.statusId)], Immutable.OrderedSet())),
|
descendants: getStatusDescendants(state, Number(props.params.statusId)),
|
||||||
me: state.getIn(['timelines', 'me'])
|
me: state.getIn(['timelines', 'me'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ComposeForm from '../components/compose_form';
|
import ComposeForm from '../components/compose_form';
|
||||||
import { changeCompose, submitCompose, cancelReplyCompose } from '../../../actions/compose';
|
import { changeCompose, submitCompose, cancelReplyCompose } from '../../../actions/compose';
|
||||||
import { selectStatus } from '../../../reducers/timelines';
|
import { getStatus } from '../../../selectors';
|
||||||
|
|
||||||
const mapStateToProps = function (state, props) {
|
const mapStateToProps = function (state, props) {
|
||||||
return {
|
return {
|
||||||
text: state.getIn(['compose', 'text']),
|
text: state.getIn(['compose', 'text']),
|
||||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||||
in_reply_to: selectStatus(state, state.getIn(['compose', 'in_reply_to']))
|
in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to']))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,10 @@ import {
|
||||||
dismissNotification,
|
dismissNotification,
|
||||||
clearNotifications
|
clearNotifications
|
||||||
} from '../../../actions/notifications';
|
} from '../../../actions/notifications';
|
||||||
|
import { getNotifications } from '../../../selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
notifications: state.get('notifications').map((item, i) => ({
|
notifications: getNotifications(state)
|
||||||
message: item.get('message'),
|
|
||||||
title: item.get('title'),
|
|
||||||
key: item.get('key'),
|
|
||||||
dismissAfter: 5000
|
|
||||||
})).toJS()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
|
|
@ -8,14 +8,18 @@ import {
|
||||||
unfavourite
|
unfavourite
|
||||||
} from '../../../actions/interactions';
|
} from '../../../actions/interactions';
|
||||||
import { expandTimeline } from '../../../actions/timelines';
|
import { expandTimeline } from '../../../actions/timelines';
|
||||||
import { selectStatus } from '../../../reducers/timelines';
|
import { makeGetTimeline } from '../../../selectors';
|
||||||
import { deleteStatus } from '../../../actions/statuses';
|
import { deleteStatus } from '../../../actions/statuses';
|
||||||
|
|
||||||
const mapStateToProps = function (state, props) {
|
const makeMapStateToProps = () => {
|
||||||
return {
|
const getTimeline = makeGetTimeline();
|
||||||
statuses: state.getIn(['timelines', props.type]).map(id => selectStatus(state, id)),
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
statuses: getTimeline(state, props.type),
|
||||||
me: state.getIn(['timelines', 'me'])
|
me: state.getIn(['timelines', 'me'])
|
||||||
};
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = function (dispatch, props) {
|
const mapDispatchToProps = function (dispatch, props) {
|
||||||
|
@ -50,4 +54,4 @@ const mapDispatchToProps = function (dispatch, props) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
|
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
|
||||||
|
|
|
@ -40,32 +40,6 @@ const initialState = Immutable.Map({
|
||||||
relationships: Immutable.Map()
|
relationships: Immutable.Map()
|
||||||
});
|
});
|
||||||
|
|
||||||
export function selectStatus(state, id) {
|
|
||||||
let status = state.getIn(['timelines', 'statuses', id], null);
|
|
||||||
|
|
||||||
if (status === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = status.set('account', selectAccount(state, status.get('account')));
|
|
||||||
|
|
||||||
if (status.get('reblog') !== null) {
|
|
||||||
status = status.set('reblog', selectStatus(state, status.get('reblog')));
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function selectAccount(state, id) {
|
|
||||||
let account = state.getIn(['timelines', 'accounts', id], null);
|
|
||||||
|
|
||||||
if (account === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return account.set('relationship', state.getIn(['timelines', 'relationships', id]));
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeStatus(state, status) {
|
function normalizeStatus(state, status) {
|
||||||
// Separate account
|
// Separate account
|
||||||
let account = status.get('account');
|
let account = status.get('account');
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
|
const getStatuses = state => state.getIn(['timelines', 'statuses']);
|
||||||
|
const getAccounts = state => state.getIn(['timelines', 'accounts']);
|
||||||
|
|
||||||
|
const getAccountBase = (state, id) => state.getIn(['timelines', 'accounts', id], null);
|
||||||
|
const getAccountRelationship = (state, id) => state.getIn(['timelines', 'relationships', id]);
|
||||||
|
|
||||||
|
export const getAccount = createSelector([getAccountBase, getAccountRelationship], (base, relationship) => {
|
||||||
|
if (base === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.set('relationship', relationship);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStatusBase = (state, id) => state.getIn(['timelines', 'statuses', id], null);
|
||||||
|
|
||||||
|
export const getStatus = createSelector([getStatusBase, getStatuses, getAccounts], (base, statuses, accounts) => {
|
||||||
|
if (base === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return assembleStatus(base.get('id'), statuses, accounts);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAccountTimelineIds = (state, id) => state.getIn(['timelines', 'accounts_timelines', id], Immutable.List());
|
||||||
|
|
||||||
|
const assembleStatus = (id, statuses, accounts) => {
|
||||||
|
let status = statuses.get(id);
|
||||||
|
|
||||||
|
if (status === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reblog = statuses.get(status.get('reblog'), null);
|
||||||
|
|
||||||
|
if (reblog !== null) {
|
||||||
|
reblog = reblog.set('account', accounts.get(reblog.get('account')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.set('reblog', reblog).set('account', accounts.get(status.get('account')));
|
||||||
|
};
|
||||||
|
|
||||||
|
const assembleStatusList = (ids, statuses, accounts) => {
|
||||||
|
return ids.map(statusId => assembleStatus(statusId, statuses, accounts)).filterNot(status => status === null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAccountTimeline = createSelector([getAccountTimelineIds, getStatuses, getAccounts], assembleStatusList);
|
||||||
|
|
||||||
|
const getTimelineIds = (state, timelineType) => state.getIn(['timelines', timelineType]);
|
||||||
|
|
||||||
|
export const makeGetTimeline = () => {
|
||||||
|
return createSelector([getTimelineIds, getStatuses, getAccounts], assembleStatusList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusAncestorsIds = (state, id) => state.getIn(['timelines', 'ancestors', id], Immutable.OrderedSet());
|
||||||
|
|
||||||
|
export const getStatusAncestors = createSelector([getStatusAncestorsIds, getStatuses, getAccounts], assembleStatusList);
|
||||||
|
|
||||||
|
const getStatusDescendantsIds = (state, id) => state.getIn(['timelines', 'descendants', id], Immutable.OrderedSet());
|
||||||
|
|
||||||
|
export const getStatusDescendants = createSelector([getStatusDescendantsIds, getStatuses, getAccounts], assembleStatusList);
|
||||||
|
|
||||||
|
const getNotificationsBase = state => state.get('notifications');
|
||||||
|
|
||||||
|
export const getNotifications = createSelector([getNotificationsBase], (base) => {
|
||||||
|
let arr = [];
|
||||||
|
|
||||||
|
base.forEach(item => {
|
||||||
|
arr.push({
|
||||||
|
message: item.get('message'),
|
||||||
|
title: item.get('title'),
|
||||||
|
key: item.get('key'),
|
||||||
|
dismissAfter: 5000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
});
|
|
@ -198,7 +198,7 @@
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
width: 120px;
|
width: 100px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: #d9e1e8;
|
background: #d9e1e8;
|
||||||
color: #282c37;
|
color: #282c37;
|
||||||
|
|
|
@ -17,15 +17,17 @@
|
||||||
"es6-promise": "^3.2.1",
|
"es6-promise": "^3.2.1",
|
||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
"moment": "^2.14.1",
|
"moment": "^2.14.1",
|
||||||
|
"react-addons-perf": "^15.3.2",
|
||||||
"react-addons-pure-render-mixin": "^15.3.1",
|
"react-addons-pure-render-mixin": "^15.3.1",
|
||||||
"react-immutable-proptypes": "^2.1.0",
|
"react-immutable-proptypes": "^2.1.0",
|
||||||
"react-notification": "^6.1.1",
|
"react-notification": "^6.1.1",
|
||||||
"react-redux": "^4.4.5",
|
"react-redux": "^5.0.0-beta.3",
|
||||||
"react-redux-loading-bar": "^2.3.3",
|
"react-redux-loading-bar": "^2.3.3",
|
||||||
"react-router": "^2.8.0",
|
"react-router": "^2.8.0",
|
||||||
"react-simple-dropdown": "^1.1.4",
|
"react-simple-dropdown": "^1.1.4",
|
||||||
"redux": "^3.5.2",
|
"redux": "^3.5.2",
|
||||||
"redux-immutable": "^3.0.8",
|
"redux-immutable": "^3.0.8",
|
||||||
"redux-thunk": "^2.1.0"
|
"redux-thunk": "^2.1.0",
|
||||||
|
"reselect": "^2.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue