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

Improve performance of compose form

parent 5997bb47
No related branches found
No related tags found
No related merge requests found
Showing
with 169 additions and 99 deletions
......@@ -2,7 +2,7 @@ import CharacterCounter from './character_counter';
import Button from '../../../components/button';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ReplyIndicator from './reply_indicator';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import UploadButton from './upload_button';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import AutosuggestAccountContainer from '../../compose/containers/autosuggest_account_container';
......@@ -11,6 +11,7 @@ import UploadButtonContainer from '../containers/upload_button_container';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import Collapsable from '../../../components/collapsable';
import UnlistedToggleContainer from '../containers/unlisted_toggle_container';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
......@@ -31,24 +32,23 @@ const ComposeForm = React.createClass({
unlisted: React.PropTypes.bool,
private: React.PropTypes.bool,
fileDropDate: React.PropTypes.instanceOf(Date),
focusDate: React.PropTypes.instanceOf(Date),
preselectDate: React.PropTypes.instanceOf(Date),
is_submitting: React.PropTypes.bool,
is_uploading: React.PropTypes.bool,
in_reply_to: ImmutablePropTypes.map,
media_count: React.PropTypes.number,
me: React.PropTypes.number,
needsPrivacyWarning: React.PropTypes.bool,
mentionedDomains: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired,
onCancelReply: React.PropTypes.func.isRequired,
onClearSuggestions: React.PropTypes.func.isRequired,
onFetchSuggestions: React.PropTypes.func.isRequired,
onSuggestionSelected: React.PropTypes.func.isRequired,
onChangeSensitivity: React.PropTypes.func.isRequired,
onChangeSpoilerness: React.PropTypes.func.isRequired,
onChangeSpoilerText: React.PropTypes.func.isRequired,
onChangeVisibility: React.PropTypes.func.isRequired,
onChangeListability: React.PropTypes.func.isRequired,
onChangeVisibility: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
......@@ -97,17 +97,13 @@ const ComposeForm = React.createClass({
this.props.onChangeVisibility(e.target.checked);
},
handleChangeListability (e) {
this.props.onChangeListability(e.target.checked);
},
componentDidUpdate (prevProps) {
if ((prevProps.in_reply_to === null && this.props.in_reply_to !== null) || (prevProps.in_reply_to !== null && this.props.in_reply_to !== null && prevProps.in_reply_to.get('id') !== this.props.in_reply_to.get('id'))) {
if (this.props.focusDate !== prevProps.focusDate) {
// If replying to zero or one users, places the cursor at the end of the textbox.
// If replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
const selectionStart = this.props.text.search(/\s/) + 1;
const selectionEnd = this.props.text.length;
const selectionStart = (this.props.preselectDate !== prevProps.preselectDate) ? (this.props.text.search(/\s/) + 1) : selectionEnd;
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
this.autosuggestTextarea.textarea.focus();
......@@ -122,14 +118,9 @@ const ComposeForm = React.createClass({
const { intl, needsPrivacyWarning, mentionedDomains } = this.props;
const disabled = this.props.is_submitting || this.props.is_uploading;
let replyArea = '';
let publishText = '';
let privacyWarning = '';
let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
if (this.props.in_reply_to) {
replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
}
let reply_to_other = false;
if (needsPrivacyWarning) {
privacyWarning = (
......@@ -158,7 +149,8 @@ const ComposeForm = React.createClass({
</Collapsable>
{privacyWarning}
{replyArea}
<ReplyIndicatorContainer />
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
......@@ -190,12 +182,7 @@ const ComposeForm = React.createClass({
<span className='compose-form__label__text'><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
</label>
<Collapsable isVisible={!(this.props.private || reply_to_other)} fullHeight={39.5}>
<label className='compose-form__label'>
<Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
<span className='compose-form__label__text'><FormattedMessage id='compose_form.unlisted' defaultMessage='Do not display on public timelines' /></span>
</label>
</Collapsable>
<UnlistedToggleContainer />
<Collapsable isVisible={this.props.media_count > 0} fullHeight={39.5}>
<label className='compose-form__label'>
......
......@@ -17,7 +17,7 @@ const ReplyIndicator = React.createClass({
},
propTypes: {
status: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map,
onCancel: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
......@@ -36,17 +36,22 @@ const ReplyIndicator = React.createClass({
},
render () {
const { intl } = this.props;
const content = { __html: emojify(this.props.status.get('content')) };
const { status, intl } = this.props;
if (!status) {
return null;
}
const content = { __html: emojify(status.get('content')) };
return (
<div className='reply-indicator'>
<div style={{ overflow: 'hidden', marginBottom: '5px' }}>
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
<a href={this.props.status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={this.props.status.getIn(['account', 'avatar'])} /></div>
<DisplayName account={this.props.status.get('account')} />
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
<DisplayName account={status.get('account')} />
</a>
</div>
......
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import Collapsable from '../../../components/collapsable';
const UnlistedToggle = React.createClass({
propTypes: {
isPrivate: React.PropTypes.bool,
isUnlisted: React.PropTypes.bool,
isReplyToOther: React.PropTypes.bool,
onChangeListability: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
render () {
const { isPrivate, isUnlisted, isReplyToOther, onChangeListability } = this.props;
return (
<Collapsable isVisible={!(isPrivate || isReplyToOther)} fullHeight={39.5}>
<label className='compose-form__label'>
<Toggle checked={isUnlisted} onChange={onChangeListability} />
<span className='compose-form__label__text'><FormattedMessage id='compose_form.unlisted' defaultMessage='Do not display on public timelines' /></span>
</label>
</Collapsable>
);
}
});
export default UnlistedToggle;
......@@ -3,7 +3,6 @@ import ComposeForm from '../components/compose_form';
import {
changeCompose,
submitCompose,
cancelReplyCompose,
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
......@@ -13,83 +12,69 @@ import {
changeComposeVisibility,
changeComposeListability
} from '../../../actions/compose';
import { makeGetStatus } from '../../../selectors';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => {
const mentionedUsernamesWithDomains = state.getIn(['compose', 'text']).match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig);
const mapStateToProps = function (state, props) {
const mentionedUsernamesWithDomains = state.getIn(['compose', 'text']).match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig);
return {
text: state.getIn(['compose', 'text']),
suggestion_token: state.getIn(['compose', 'suggestion_token']),
suggestions: state.getIn(['compose', 'suggestions']),
sensitive: state.getIn(['compose', 'sensitive']),
spoiler: state.getIn(['compose', 'spoiler']),
spoiler_text: state.getIn(['compose', 'spoiler_text']),
unlisted: state.getIn(['compose', 'unlisted'], ),
private: state.getIn(['compose', 'private']),
fileDropDate: state.getIn(['compose', 'fileDropDate']),
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'])),
media_count: state.getIn(['compose', 'media_attachments']).size,
me: state.getIn(['compose', 'me']),
needsPrivacyWarning: state.getIn(['compose', 'private']) && mentionedUsernamesWithDomains !== null,
mentionedDomains: mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : []
};
return {
text: state.getIn(['compose', 'text']),
suggestion_token: state.getIn(['compose', 'suggestion_token']),
suggestions: state.getIn(['compose', 'suggestions']),
sensitive: state.getIn(['compose', 'sensitive']),
spoiler: state.getIn(['compose', 'spoiler']),
spoiler_text: state.getIn(['compose', 'spoiler_text']),
unlisted: state.getIn(['compose', 'unlisted'], ),
private: state.getIn(['compose', 'private']),
fileDropDate: state.getIn(['compose', 'fileDropDate']),
focusDate: state.getIn(['compose', 'focusDate']),
preselectDate: state.getIn(['compose', 'preselectDate']),
is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']),
media_count: state.getIn(['compose', 'media_attachments']).size,
me: state.getIn(['compose', 'me']),
needsPrivacyWarning: state.getIn(['compose', 'private']) && mentionedUsernamesWithDomains !== null,
mentionedDomains: mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : []
};
return mapStateToProps;
};
const mapDispatchToProps = function (dispatch) {
return {
onChange (text) {
dispatch(changeCompose(text));
},
const mapDispatchToProps = (dispatch) => ({
onSubmit () {
dispatch(submitCompose());
},
onChange (text) {
dispatch(changeCompose(text));
},
onCancelReply () {
dispatch(cancelReplyCompose());
},
onSubmit () {
dispatch(submitCompose());
},
onClearSuggestions () {
dispatch(clearComposeSuggestions());
},
onClearSuggestions () {
dispatch(clearComposeSuggestions());
},
onFetchSuggestions (token) {
dispatch(fetchComposeSuggestions(token));
},
onFetchSuggestions (token) {
dispatch(fetchComposeSuggestions(token));
},
onSuggestionSelected (position, token, accountId) {
dispatch(selectComposeSuggestion(position, token, accountId));
},
onSuggestionSelected (position, token, accountId) {
dispatch(selectComposeSuggestion(position, token, accountId));
},
onChangeSensitivity (checked) {
dispatch(changeComposeSensitivity(checked));
},
onChangeSensitivity (checked) {
dispatch(changeComposeSensitivity(checked));
},
onChangeSpoilerness (checked) {
dispatch(changeComposeSpoilerness(checked));
},
onChangeSpoilerness (checked) {
dispatch(changeComposeSpoilerness(checked));
},
onChangeSpoilerText (checked) {
dispatch(changeComposeSpoilerText(checked));
},
onChangeSpoilerText (checked) {
dispatch(changeComposeSpoilerText(checked));
},
onChangeVisibility (checked) {
dispatch(changeComposeVisibility(checked));
},
onChangeVisibility (checked) {
dispatch(changeComposeVisibility(checked));
},
onChangeListability (checked) {
dispatch(changeComposeListability(checked));
}
}
};
});
export default connect(makeMapStateToProps, mapDispatchToProps)(ComposeForm);
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
import { connect } from 'react-redux';
import { cancelReplyCompose } from '../../../actions/compose';
import { makeGetStatus } from '../../../selectors';
import ReplyIndicator from '../components/reply_indicator';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => ({
status: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
});
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
onCancel () {
dispatch(cancelReplyCompose());
}
});
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
import { connect } from 'react-redux';
import UnlistedToggle from '../components/unlisted_toggle';
import { makeGetStatus } from '../../../selectors';
import { changeComposeListability } from '../../../actions/compose';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = state => {
const status = getStatus(state, state.getIn(['compose', 'in_reply_to']));
const me = state.getIn(['compose', 'me']);
return {
isPrivate: state.getIn(['compose', 'private']),
isUnlisted: state.getIn(['compose', 'unlisted']),
isReplyToOther: status ? status.getIn(['account', 'id']) !== me : false
};
};
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
onChangeListability (e) {
dispatch(changeComposeListability(e.target.checked));
}
});
export default connect(makeMapStateToProps, mapDispatchToProps)(UnlistedToggle);
......@@ -54,12 +54,12 @@ const mapDispatchToProps = (dispatch, { type, id }) => ({
dispatch(expandTimeline(type, id));
},
@debounce(300)
@debounce(100)
onScrollToTop () {
dispatch(scrollTopTimeline(type, true));
},
@debounce(500)
@debounce(100)
onScroll () {
dispatch(scrollTopTimeline(type, false));
}
......
......@@ -35,6 +35,8 @@ const initialState = Immutable.Map({
private: false,
text: '',
fileDropDate: null,
focusDate: null,
preselectDate: null,
in_reply_to: null,
is_submitting: false,
is_uploading: false,
......@@ -99,6 +101,7 @@ const insertSuggestion = (state, position, token, completion) => {
map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
map.set('suggestion_token', null);
map.update('suggestions', Immutable.List(), list => list.clear());
map.set('focusDate', new Date());
});
};
......@@ -128,6 +131,8 @@ export default function compose(state = initialState, action) {
map.set('text', statusToTextMentions(state, action.status));
map.set('unlisted', action.status.get('visibility') === 'unlisted' || state.get('default_privacy') === 'unlisted');
map.set('private', action.status.get('visibility') === 'private' || state.get('default_privacy') === 'private');
map.set('focusDate', new Date());
map.set('preselectDate', new Date());
});
case COMPOSE_REPLY_CANCEL:
return state.withMutations(map => {
......@@ -156,7 +161,7 @@ export default function compose(state = initialState, action) {
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')} `);
return state.update('text', text => `${text}@${action.account.get('acct')} `).set('focusDate', new Date());
case COMPOSE_SUGGESTIONS_CLEAR:
return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null);
case COMPOSE_SUGGESTIONS_READY:
......
......@@ -249,6 +249,7 @@ const resetTimeline = (state, timeline, id) => {
.set('isLoading', true)
.set('loaded', false)
.set('next', null)
.set('top', true)
.update('items', list => list.clear()));
} else {
state = state.setIn([timeline, 'isLoading'], true);
......
......@@ -1080,7 +1080,7 @@ button.active i.fa-retweet {
flex: 0 0 auto;
cursor: pointer;
&.active {
&.active .fa {
color: $color4;
text-shadow: 0 0 10px rgba($color4, 0.4);
}
......
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