From f8f40f15dafca65dc07d5c5c19fb9a9dc3473dd6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 24 Oct 2016 17:11:02 +0200 Subject: [PATCH] Move status components inside individual containers. We still need to select all statuses/accounts to assemble, but at least lists don't have to be re-rendered all the time now. Also add "mention" dropdown option --- .../components/actions/compose.jsx | 8 +++ .../components/status_action_bar.jsx | 9 ++- .../components/components/status_list.jsx | 16 ++--- .../containers/status_container.jsx | 59 +++++++++++++++++++ .../account/components/action_bar.jsx | 19 +++--- .../components/features/account/index.jsx | 7 ++- .../features/account_timeline/index.jsx | 46 +++------------ .../features/status/components/action_bar.jsx | 3 + .../components/features/status/index.jsx | 55 ++++++++++++----- .../ui/containers/compose_form_container.jsx | 22 ++++--- .../ui/containers/status_list_container.jsx | 48 ++------------- .../components/reducers/compose.jsx | 5 +- .../components/selectors/index.jsx | 36 +++-------- 13 files changed, 179 insertions(+), 154 deletions(-) create mode 100644 app/assets/javascripts/components/containers/status_container.jsx diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 402c59dc6..1bf95eec0 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -6,6 +6,7 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; +export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; @@ -32,6 +33,13 @@ export function cancelReplyCompose() { }; }; +export function mentionCompose(account) { + return { + type: COMPOSE_MENTION, + account: account + }; +}; + export function submitCompose() { return function (dispatch, getState) { dispatch(submitComposeRequest()); diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index 945b722a6..23c8e4d5c 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -9,7 +9,8 @@ const StatusActionBar = React.createClass({ onReply: React.PropTypes.func, onFavourite: React.PropTypes.func, onReblog: React.PropTypes.func, - onDelete: React.PropTypes.func + onDelete: React.PropTypes.func, + onMention: React.PropTypes.func }, mixins: [PureRenderMixin], @@ -30,12 +31,18 @@ const StatusActionBar = React.createClass({ this.props.onDelete(this.props.status); }, + handleMentionClick () { + this.props.onMention(this.props.status.get('account')); + }, + render () { const { status, me } = this.props; let menu = []; if (status.getIn(['account', 'id']) === me) { menu.push({ text: 'Delete', action: this.handleDeleteClick }); + } else { + menu.push({ text: 'Mention', action: this.handleMentionClick }); } return ( diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx index 4977d84ce..b4463b69c 100644 --- a/app/assets/javascripts/components/components/status_list.jsx +++ b/app/assets/javascripts/components/components/status_list.jsx @@ -2,18 +2,14 @@ import Status from './status'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import { ScrollContainer } from 'react-router-scroll'; +import StatusContainer from '../containers/status_container'; const StatusList = React.createClass({ propTypes: { - statuses: ImmutablePropTypes.list.isRequired, - onReply: React.PropTypes.func, - onReblog: React.PropTypes.func, - onFavourite: React.PropTypes.func, - onDelete: React.PropTypes.func, + statusIds: ImmutablePropTypes.list.isRequired, onScrollToBottom: React.PropTypes.func, - trackScroll: React.PropTypes.bool, - me: React.PropTypes.number + trackScroll: React.PropTypes.bool }, getDefaultProps () { @@ -33,13 +29,13 @@ const StatusList = React.createClass({ }, render () { - const { statuses, onScrollToBottom, trackScroll, ...other } = this.props; + const { statusIds, onScrollToBottom, trackScroll } = this.props; const scrollableArea = (
- {statuses.map((status) => { - return ; + {statusIds.map((statusId) => { + return ; })}
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx new file mode 100644 index 000000000..b4d3740d9 --- /dev/null +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -0,0 +1,59 @@ +import { connect } from 'react-redux'; +import Status from '../components/status'; +import { makeGetStatus } from '../selectors'; +import { + replyCompose, + mentionCompose +} from '../actions/compose'; +import { + reblog, + favourite, + unreblog, + unfavourite +} from '../actions/interactions'; +import { deleteStatus } from '../actions/statuses'; + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, props) => ({ + status: getStatus(state, props.id), + me: state.getIn(['timelines', 'me']) + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch) => ({ + + onReply (status) { + dispatch(replyCompose(status)); + }, + + onReblog (status) { + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else { + dispatch(reblog(status)); + } + }, + + onFavourite (status) { + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } + }, + + onDelete (status) { + dispatch(deleteStatus(status.get('id'))); + }, + + onMention (account) { + dispatch(mentionCompose(account)); + } + +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(Status); diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx index 0f26b1e5a..195b143af 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -8,7 +8,8 @@ const ActionBar = React.createClass({ account: ImmutablePropTypes.map.isRequired, me: React.PropTypes.number.isRequired, onFollow: React.PropTypes.func.isRequired, - onBlock: React.PropTypes.func.isRequired + onBlock: React.PropTypes.func.isRequired, + onMention: React.PropTypes.func.isRequired }, mixins: [PureRenderMixin], @@ -18,6 +19,8 @@ const ActionBar = React.createClass({ let menu = []; + menu.push({ text: 'Mention', action: this.props.onMention }); + if (account.get('id') === me) { menu.push({ text: 'Edit profile', href: '/settings/profile' }); } else if (account.getIn(['relationship', 'blocking'])) { @@ -32,26 +35,26 @@ const ActionBar = React.createClass({ return (
+
+ +
+
-
+
Posts {account.get('statuses_count')}
-
+
Follows {account.get('following_count')}
-
+
Followers {account.get('followers_count')}
- -
- -
); }, diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx index 83770eb74..76d69f751 100644 --- a/app/assets/javascripts/components/features/account/index.jsx +++ b/app/assets/javascripts/components/features/account/index.jsx @@ -10,6 +10,7 @@ import { fetchAccountTimeline, expandAccountTimeline } from '../../actions/accounts'; +import { mentionCompose } from '../../actions/compose'; import Header from './components/header'; import { getAccountTimeline, @@ -62,6 +63,10 @@ const Account = React.createClass({ } }, + handleMention () { + this.props.dispatch(mentionCompose(this.props.account)); + }, + render () { const { account, me } = this.props; @@ -78,7 +83,7 @@ const Account = React.createClass({
- + {this.props.children} diff --git a/app/assets/javascripts/components/features/account_timeline/index.jsx b/app/assets/javascripts/components/features/account_timeline/index.jsx index 0b3d641d5..f79570361 100644 --- a/app/assets/javascripts/components/features/account_timeline/index.jsx +++ b/app/assets/javascripts/components/features/account_timeline/index.jsx @@ -1,23 +1,15 @@ import { connect } from 'react-redux'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { getAccountTimeline } from '../../selectors'; import { fetchAccountTimeline, expandAccountTimeline } from '../../actions/accounts'; -import { deleteStatus } from '../../actions/statuses'; -import { replyCompose } from '../../actions/compose'; -import { - favourite, - reblog, - unreblog, - unfavourite -} from '../../actions/interactions'; import StatusList from '../../components/status_list'; +import LoadingIndicator from '../../components/loading_indicator'; const mapStateToProps = (state, props) => ({ - statuses: getAccountTimeline(state, Number(props.params.accountId)), + statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId)]), me: state.getIn(['timelines', 'me']) }); @@ -26,7 +18,7 @@ const AccountTimeline = React.createClass({ propTypes: { params: React.PropTypes.object.isRequired, dispatch: React.PropTypes.func.isRequired, - statuses: ImmutablePropTypes.list + statusIds: ImmutablePropTypes.list }, mixins: [PureRenderMixin], @@ -41,38 +33,18 @@ const AccountTimeline = React.createClass({ } }, - handleReply (status) { - this.props.dispatch(replyCompose(status)); - }, - - handleReblog (status) { - if (status.get('reblogged')) { - this.props.dispatch(unreblog(status)); - } else { - this.props.dispatch(reblog(status)); - } - }, - - handleFavourite (status) { - if (status.get('favourited')) { - this.props.dispatch(unfavourite(status)); - } else { - this.props.dispatch(favourite(status)); - } - }, - - handleDelete (status) { - this.props.dispatch(deleteStatus(status.get('id'))); - }, - handleScrollToBottom () { this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId))); }, render () { - const { statuses, me } = this.props; + const { statusIds, me } = this.props; - return + if (!statusIds) { + return ; + } + + return } }); diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx index 6d6aa87fc..d0cae4557 100644 --- a/app/assets/javascripts/components/features/status/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx @@ -11,6 +11,7 @@ const ActionBar = React.createClass({ onReblog: React.PropTypes.func.isRequired, onFavourite: React.PropTypes.func.isRequired, onDelete: React.PropTypes.func.isRequired, + onMention: React.PropTypes.func.isRequired, me: React.PropTypes.number.isRequired }, @@ -23,6 +24,8 @@ const ActionBar = React.createClass({ if (me === status.getIn(['account', 'id'])) { menu.push({ text: 'Delete', action: () => this.props.onDelete(status) }); + } else { + menu.push({ text: 'Mention', action: () => this.props.onMention(status.get('account')) }); } return ( diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index c51fb5d31..f4ca8ff92 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -9,22 +9,32 @@ import DetailedStatus from './components/detailed_status'; import ActionBar from './components/action_bar'; import Column from '../ui/components/column'; import { favourite, reblog } from '../../actions/interactions'; -import { replyCompose } from '../../actions/compose'; +import { + replyCompose, + mentionCompose +} from '../../actions/compose'; import { deleteStatus } from '../../actions/statuses'; import { - getStatus, + makeGetStatus, getStatusAncestors, getStatusDescendants } from '../../selectors'; import { ScrollContainer } from 'react-router-scroll'; import ColumnBackButton from '../../components/column_back_button'; +import StatusContainer from '../../containers/status_container'; -const mapStateToProps = (state, props) => ({ - status: getStatus(state, Number(props.params.statusId)), - ancestors: getStatusAncestors(state, Number(props.params.statusId)), - descendants: getStatusDescendants(state, Number(props.params.statusId)), - me: state.getIn(['timelines', 'me']) -}); +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, props) => ({ + status: getStatus(state, Number(props.params.statusId)), + ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]), + descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]), + me: state.getIn(['timelines', 'me']) + }); + + return mapStateToProps; +}; const Status = React.createClass({ @@ -32,8 +42,8 @@ const Status = React.createClass({ params: React.PropTypes.object.isRequired, dispatch: React.PropTypes.func.isRequired, status: ImmutablePropTypes.map, - ancestors: ImmutablePropTypes.orderedSet.isRequired, - descendants: ImmutablePropTypes.orderedSet.isRequired + ancestorsIds: ImmutablePropTypes.orderedSet, + descendantsIds: ImmutablePropTypes.orderedSet }, mixins: [PureRenderMixin], @@ -64,12 +74,17 @@ const Status = React.createClass({ this.props.dispatch(deleteStatus(status.get('id'))); }, + handleMentionClick (account) { + this.props.dispatch(mentionCompose(account)); + }, + renderChildren (list) { - return list.map(s => ); + return list.map(id => ); }, render () { - const { status, ancestors, descendants, me } = this.props; + let ancestors, descendants; + const { status, ancestorsIds, descendantsIds, me } = this.props; if (status === null) { return ( @@ -81,18 +96,26 @@ const Status = React.createClass({ const account = status.get('account'); + if (ancestorsIds) { + ancestors =
{this.renderChildren(ancestorsIds)}
; + } + + if (descendantsIds) { + descendants =
{this.renderChildren(descendantsIds)}
; + } + return (
-
{this.renderChildren(ancestors)}
+ {ancestors} - + -
{this.renderChildren(descendants)}
+ {descendants}
@@ -101,4 +124,4 @@ const Status = React.createClass({ }); -export default connect(mapStateToProps)(Status); +export default connect(makeMapStateToProps)(Status); diff --git a/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx index 747eb9691..163d6fa20 100644 --- a/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx @@ -1,15 +1,21 @@ import { connect } from 'react-redux'; import ComposeForm from '../components/compose_form'; import { changeCompose, submitCompose, cancelReplyCompose } from '../../../actions/compose'; -import { getStatus } from '../../../selectors'; +import { makeGetStatus } from '../../../selectors'; -const mapStateToProps = function (state, props) { - return { - text: state.getIn(['compose', 'text']), - is_submitting: state.getIn(['compose', 'is_submitting']), - is_uploading: state.getIn(['compose', 'is_uploading']), - in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])) +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = function (state, props) { + return { + text: state.getIn(['compose', 'text']), + is_submitting: state.getIn(['compose', 'is_submitting']), + is_uploading: state.getIn(['compose', 'is_uploading']), + in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])) + }; }; + + return mapStateToProps; }; const mapDispatchToProps = function (dispatch) { @@ -28,4 +34,4 @@ const mapDispatchToProps = function (dispatch) { } }; -export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm); +export default connect(makeMapStateToProps, mapDispatchToProps)(ComposeForm); diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx index 045cc59d1..213435a06 100644 --- a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx @@ -1,57 +1,17 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; -import { replyCompose } from '../../../actions/compose'; -import { - reblog, - favourite, - unreblog, - unfavourite -} from '../../../actions/interactions'; import { expandTimeline } from '../../../actions/timelines'; -import { makeGetTimeline } from '../../../selectors'; -import { deleteStatus } from '../../../actions/statuses'; -const makeMapStateToProps = () => { - const getTimeline = makeGetTimeline(); - - const mapStateToProps = (state, props) => ({ - statuses: getTimeline(state, props.type), - me: state.getIn(['timelines', 'me']) - }); - - return mapStateToProps; -}; +const mapStateToProps = (state, props) => ({ + statusIds: state.getIn(['timelines', props.type]) +}); const mapDispatchToProps = function (dispatch, props) { return { - onReply (status) { - dispatch(replyCompose(status)); - }, - - onFavourite (status) { - if (status.get('favourited')) { - dispatch(unfavourite(status)); - } else { - dispatch(favourite(status)); - } - }, - - onReblog (status) { - if (status.get('reblogged')) { - dispatch(unreblog(status)); - } else { - dispatch(reblog(status)); - } - }, - onScrollToBottom () { dispatch(expandTimeline(props.type)); - }, - - onDelete (status) { - dispatch(deleteStatus(status.get('id'))); } }; }; -export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); +export default connect(mapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx index 9c9ce566e..f2fd3ad80 100644 --- a/app/assets/javascripts/components/reducers/compose.jsx +++ b/app/assets/javascripts/components/reducers/compose.jsx @@ -2,6 +2,7 @@ import { COMPOSE_CHANGE, COMPOSE_REPLY, COMPOSE_REPLY_CANCEL, + COMPOSE_MENTION, COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_SUCCESS, COMPOSE_SUBMIT_FAIL, @@ -32,7 +33,7 @@ function statusToTextMentions(state, status) { if (status.getIn(['account', 'id']) !== me) { set = set.add(`@${status.getIn(['account', 'acct'])} `); } - + return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join(''); }; @@ -92,6 +93,8 @@ export default function compose(state = initialState, action) { return removeMedia(state, action.media_id); case COMPOSE_UPLOAD_PROGRESS: return state.set('progress', Math.round((action.loaded / action.total) * 100)); + case COMPOSE_MENTION: + return state.update('text', text => `${text}@${action.account.get('acct')} `); case TIMELINE_DELETE: if (action.id === state.get('in_reply_to')) { return state.set('in_reply_to', null); diff --git a/app/assets/javascripts/components/selectors/index.jsx b/app/assets/javascripts/components/selectors/index.jsx index 91f900f90..b571e43d5 100644 --- a/app/assets/javascripts/components/selectors/index.jsx +++ b/app/assets/javascripts/components/selectors/index.jsx @@ -17,15 +17,15 @@ export const getAccount = createSelector([getAccountBase, getAccountRelationship 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; - } +export const makeGetStatus = () => { + return 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()); + return assembleStatus(base.get('id'), statuses, accounts); + }); +}; const assembleStatus = (id, statuses, accounts) => { let status = statuses.get(id, null); @@ -48,26 +48,6 @@ const assembleStatus = (id, statuses, accounts) => { 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) => {