Skip to content
Snippets Groups Projects
Commit f8f40f15 authored by Eugen Rochko's avatar Eugen Rochko
Browse files

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
parent 61db14bc
No related branches found
No related tags found
No related merge requests found
Showing
with 179 additions and 154 deletions
......@@ -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());
......
......@@ -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 (
......
......@@ -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 = (
<div style={{ overflowY: 'scroll', flex: '1 1 auto', overflowX: 'hidden' }} className='scrollable' onScroll={this.handleScroll}>
<div>
{statuses.map((status) => {
return <Status key={status.get('id')} {...other} status={status} />;
{statusIds.map((statusId) => {
return <StatusContainer key={statusId} id={statusId} />;
})}
</div>
</div>
......
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);
......@@ -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 (
<div style={{ borderTop: '1px solid #363c4b', borderBottom: '1px solid #363c4b', lineHeight: '36px', overflow: 'hidden', flex: '0 0 auto', display: 'flex' }}>
<div style={{ padding: '10px', flex: '1 1 auto' }}>
<DropdownMenu items={menu} icon='bars' size={24} />
</div>
<div style={{ flex: '1 1 auto', display: 'flex', lineHeight: '18px' }}>
<div style={{ overflow: 'hidden', width: '80px', borderRight: '1px solid #363c4b', padding: '10px', paddingRight: '5px' }}>
<div style={{ overflow: 'hidden', width: '80px', borderLeft: '1px solid #363c4b', padding: '10px', paddingRight: '5px' }}>
<span style={{ display: 'block', textTransform: 'uppercase', fontSize: '11px', color: '#616b86' }}>Posts</span>
<span style={{ display: 'block', fontSize: '15px', fontWeight: '500', color: '#fff' }}>{account.get('statuses_count')}</span>
</div>
<div style={{ overflow: 'hidden', width: '80px', borderRight: '1px solid #363c4b', padding: '10px 5px' }}>
<div style={{ overflow: 'hidden', width: '80px', borderLeft: '1px solid #363c4b', padding: '10px 5px' }}>
<span style={{ display: 'block', textTransform: 'uppercase', fontSize: '11px', color: '#616b86' }}>Follows</span>
<span style={{ display: 'block', fontSize: '15px', fontWeight: '500', color: '#fff' }}>{account.get('following_count')}</span>
</div>
<div style={{ overflow: 'hidden', width: '80px', padding: '10px 5px', borderRight: '1px solid #363c4b' }}>
<div style={{ overflow: 'hidden', width: '80px', padding: '10px 5px', borderLeft: '1px solid #363c4b' }}>
<span style={{ display: 'block', textTransform: 'uppercase', fontSize: '11px', color: '#616b86' }}>Followers</span>
<span style={{ display: 'block', fontSize: '15px', fontWeight: '500', color: '#fff' }}>{account.get('followers_count')}</span>
</div>
</div>
<div style={{ padding: '10px', flex: '1 1 auto' }}>
<DropdownMenu items={menu} icon='bars' size={24} />
</div>
</div>
);
},
......
......@@ -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({
<ColumnBackButton />
<Header account={account} me={me} />
<ActionBar account={account} me={me} onFollow={this.handleFollow} onBlock={this.handleBlock} />
<ActionBar account={account} me={me} onFollow={this.handleFollow} onBlock={this.handleBlock} onMention={this.handleMention} />
{this.props.children}
</Column>
......
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;
if (!statusIds) {
return <LoadingIndicator />;
}
return <StatusList statuses={statuses} me={me} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} onDelete={this.handleDelete} />
return <StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} />
}
});
......
......@@ -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 (
......
......@@ -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 => <EmbeddedStatus status={s} me={this.props.me} key={s.get('id')} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} />);
return list.map(id => <StatusContainer key={id} id={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 = <div>{this.renderChildren(ancestorsIds)}</div>;
}
if (descendantsIds) {
descendants = <div>{this.renderChildren(descendantsIds)}</div>;
}
return (
<Column>
<ColumnBackButton />
<ScrollContainer scrollKey='thread'>
<div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'>
<div>{this.renderChildren(ancestors)}</div>
{ancestors}
<DetailedStatus status={status} me={me} />
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} />
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} />
<div>{this.renderChildren(descendants)}</div>
{descendants}
</div>
</ScrollContainer>
</Column>
......@@ -101,4 +124,4 @@ const Status = React.createClass({
});
export default connect(mapStateToProps)(Status);
export default connect(makeMapStateToProps)(Status);
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);
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);
......@@ -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);
......
......@@ -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;
}
return assembleStatus(base.get('id'), statuses, accounts);
});
export const makeGetStatus = () => {
return createSelector([getStatusBase, getStatuses, getAccounts], (base, statuses, accounts) => {
if (base === null) {
return null;
}
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) => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment