diff options
Diffstat (limited to 'app/javascript/flavours/glitch/features')
9 files changed, 655 insertions, 104 deletions
diff --git a/app/javascript/flavours/glitch/features/report/category.js b/app/javascript/flavours/glitch/features/report/category.js new file mode 100644 index 000000000..ddbc82563 --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/category.js @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import Option from './components/option'; + +const messages = defineMessages({ + dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' }, + dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' }, + spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' }, + spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' }, + violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' }, + violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' }, + other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' }, + other_description: { id: 'report.reasons.other_description', defaultMessage: 'The issue does not fit into other categories' }, + status: { id: 'report.category.title_status', defaultMessage: 'post' }, + account: { id: 'report.category.title_account', defaultMessage: 'profile' }, +}); + +export default @injectIntl +class Category extends React.PureComponent { + + static propTypes = { + onNextStep: PropTypes.func.isRequired, + category: PropTypes.string, + onChangeCategory: PropTypes.func.isRequired, + startedFrom: PropTypes.oneOf(['status', 'account']), + intl: PropTypes.object.isRequired, + }; + + handleNextClick = () => { + const { onNextStep, category } = this.props; + + switch(category) { + case 'dislike': + onNextStep('thanks'); + break; + case 'violation': + onNextStep('rules'); + break; + default: + onNextStep('statuses'); + break; + } + }; + + handleCategoryToggle = (value, checked) => { + const { onChangeCategory } = this.props; + + if (checked) { + onChangeCategory(value); + } + }; + + render () { + const { category, startedFrom, intl } = this.props; + + const options = [ + 'dislike', + 'spam', + 'violation', + 'other', + ]; + + return ( + <React.Fragment> + <h3 className='report-dialog-modal__title'><FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} /></h3> + <p className='report-dialog-modal__lead'><FormattedMessage id='report.category.subtitle' defaultMessage='Choose the best match' /></p> + + <div> + {options.map(item => ( + <Option + key={item} + name='category' + value={item} + checked={category === item} + onToggle={this.handleCategoryToggle} + label={intl.formatMessage(messages[item])} + description={intl.formatMessage(messages[`${item}_description`])} + /> + ))} + </div> + + <div className='flex-spacer' /> + + <div className='report-dialog-modal__actions'> + <Button onClick={this.handleNextClick} disabled={category === null}><FormattedMessage id='report.next' defaultMessage='Next' /></Button> + </div> + </React.Fragment> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/comment.js b/app/javascript/flavours/glitch/features/report/comment.js new file mode 100644 index 000000000..b2663bbf2 --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/comment.js @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import Toggle from 'react-toggle'; + +const messages = defineMessages({ + placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' }, +}); + +export default @injectIntl +class Comment extends React.PureComponent { + + static propTypes = { + onSubmit: PropTypes.func.isRequired, + comment: PropTypes.string.isRequired, + onChangeComment: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + isSubmitting: PropTypes.bool, + forward: PropTypes.bool, + isRemote: PropTypes.bool, + domain: PropTypes.string, + onChangeForward: PropTypes.func.isRequired, + }; + + handleClick = () => { + const { onSubmit } = this.props; + onSubmit(); + }; + + handleChange = e => { + const { onChangeComment } = this.props; + onChangeComment(e.target.value); + }; + + handleKeyDown = e => { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + this.handleClick(); + } + }; + + handleForwardChange = e => { + const { onChangeForward } = this.props; + onChangeForward(e.target.checked); + }; + + render () { + const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props; + + return ( + <React.Fragment> + <h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3> + + <textarea + className='report-dialog-modal__textarea' + placeholder={intl.formatMessage(messages.placeholder)} + value={comment} + onChange={this.handleChange} + onKeyDown={this.handleKeyDown} + disabled={isSubmitting} + /> + + {isRemote && ( + <React.Fragment> + <p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p> + + <label className='report-dialog-modal__toggle'> + <Toggle checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} /> + <FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /> + </label> + </React.Fragment> + )} + + <div className='flex-spacer' /> + + <div className='report-dialog-modal__actions'> + <Button onClick={this.handleClick}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button> + </div> + </React.Fragment> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/components/option.js b/app/javascript/flavours/glitch/features/report/components/option.js new file mode 100644 index 000000000..7e94f0654 --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/components/option.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Check from 'flavours/glitch/components/check'; + +export default class Option extends React.PureComponent { + + static propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + checked: PropTypes.bool, + label: PropTypes.node, + description: PropTypes.node, + onToggle: PropTypes.func, + multiple: PropTypes.bool, + labelComponent: PropTypes.node, + }; + + handleKeyPress = e => { + const { value, checked, onToggle } = this.props; + + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + e.preventDefault(); + onToggle(value, !checked); + } + } + + handleChange = e => { + const { value, onToggle } = this.props; + onToggle(value, e.target.checked); + } + + render () { + const { name, value, checked, label, labelComponent, description, multiple } = this.props; + + return ( + <label className='dialog-option poll__option selectable'> + <input type={multiple ? 'checkbox' : 'radio'} name={name} value={value} checked={checked} onChange={this.handleChange} /> + + <span + className={classNames('poll__input', { active: checked, checkbox: multiple })} + tabIndex='0' + role='radio' + onKeyPress={this.handleKeyPress} + aria-checked={checked} + aria-label={label} + >{checked && <Check />}</span> + + {labelComponent ? labelComponent : ( + <span className='poll__option__text'> + <strong>{label}</strong> + {description} + </span> + )} + </label> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js index cc49042fc..adb5e77a7 100644 --- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js +++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js @@ -1,23 +1,32 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; import noop from 'lodash/noop'; import StatusContent from 'flavours/glitch/components/status_content'; import { MediaGallery, Video } from 'flavours/glitch/util/async-components'; import Bundle from 'flavours/glitch/features/ui/components/bundle'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import Option from './option'; export default class StatusCheckBox extends React.PureComponent { static propTypes = { + id: PropTypes.string.isRequired, status: ImmutablePropTypes.map.isRequired, checked: PropTypes.bool, onToggle: PropTypes.func.isRequired, - disabled: PropTypes.bool, + }; + + handleStatusesToggle = (value, checked) => { + const { onToggle } = this.props; + onToggle(value, checked); }; render () { - const { status, checked, onToggle, disabled } = this.props; + const { status, checked } = this.props; + let media = null; if (status.get('reblog')) { @@ -51,26 +60,45 @@ export default class StatusCheckBox extends React.PureComponent { } else { media = ( <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > - {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} revealed={false} height={110} onOpenMedia={noop} />} + {Component => ( + <Component + media={status.get('media_attachments')} + sensitive={status.get('sensitive')} + revealed={false} + height={110} + onOpenMedia={noop} + /> + )} </Bundle> ); } } - return ( - <div className='status-check-box'> - <div className='status-check-box__status'> - <StatusContent - status={status} - media={media} - /> - </div> + const labelComponent = ( + <div className='status-check-box__status poll__option__text'> + <div className='detailed-status__display-name'> + <div className='detailed-status__display-avatar'> + <Avatar account={status.get('account')} size={46} /> + </div> - <div className='status-check-box-toggle'> - <Toggle checked={checked} onChange={onToggle} disabled={disabled} /> + <div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div> </div> + + <StatusContent status={status} media={media} /> </div> ); + + return ( + <Option + name='status_ids' + value={status.get('id')} + checked={checked} + onToggle={this.handleStatusesToggle} + label={status.get('search_index')} + labelComponent={labelComponent} + multiple + /> + ); } } diff --git a/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js index 9bfd41ffc..aa34b3efd 100644 --- a/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js +++ b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js @@ -1,19 +1,15 @@ import { connect } from 'react-redux'; import StatusCheckBox from '../components/status_check_box'; -import { toggleStatusReport } from 'flavours/glitch/actions/reports'; -import { Set as ImmutableSet } from 'immutable'; +import { makeGetStatus } from 'flavours/glitch/selectors'; -const mapStateToProps = (state, { id }) => ({ - status: state.getIn(['statuses', id]), - checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id), -}); +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); -const mapDispatchToProps = (dispatch, { id }) => ({ + const mapStateToProps = (state, { id }) => ({ + status: getStatus(state, { id }), + }); - onToggle (e) { - dispatch(toggleStatusReport(id, e.target.checked)); - }, + return mapStateToProps; +}; -}); - -export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox); +export default connect(makeMapStateToProps)(StatusCheckBox); diff --git a/app/javascript/flavours/glitch/features/report/rules.js b/app/javascript/flavours/glitch/features/report/rules.js new file mode 100644 index 000000000..4772e04a2 --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/rules.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import Option from './components/option'; + +const mapStateToProps = state => ({ + rules: state.get('rules'), +}); + +export default @connect(mapStateToProps) +class Rules extends React.PureComponent { + + static propTypes = { + onNextStep: PropTypes.func.isRequired, + rules: ImmutablePropTypes.list, + selectedRuleIds: ImmutablePropTypes.set.isRequired, + onToggle: PropTypes.func.isRequired, + }; + + handleNextClick = () => { + const { onNextStep } = this.props; + onNextStep('statuses'); + }; + + handleRulesToggle = (value, checked) => { + const { onToggle } = this.props; + onToggle(value, checked); + }; + + render () { + const { rules, selectedRuleIds } = this.props; + + return ( + <React.Fragment> + <h3 className='report-dialog-modal__title'><FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' /></h3> + <p className='report-dialog-modal__lead'><FormattedMessage id='report.rules.subtitle' defaultMessage='Select all that apply' /></p> + + <div> + {rules.map(item => ( + <Option + key={item.get('id')} + name='rule_ids' + value={item.get('id')} + checked={selectedRuleIds.includes(item.get('id'))} + onToggle={this.handleRulesToggle} + label={item.get('text')} + multiple + /> + ))} + </div> + + <div className='flex-spacer' /> + + <div className='report-dialog-modal__actions'> + <Button onClick={this.handleNextClick} disabled={selectedRuleIds.size < 1}><FormattedMessage id='report.next' defaultMessage='Next' /></Button> + </div> + </React.Fragment> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/statuses.js b/app/javascript/flavours/glitch/features/report/statuses.js new file mode 100644 index 000000000..69cfbb3e5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/statuses.js @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container'; +import { OrderedSet } from 'immutable'; +import { FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; + +const mapStateToProps = (state, { accountId }) => ({ + availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])), +}); + +export default @connect(mapStateToProps) +class Statuses extends React.PureComponent { + + static propTypes = { + onNextStep: PropTypes.func.isRequired, + accountId: PropTypes.string.isRequired, + availableStatusIds: ImmutablePropTypes.set.isRequired, + selectedStatusIds: ImmutablePropTypes.set.isRequired, + onToggle: PropTypes.func.isRequired, + }; + + handleNextClick = () => { + const { onNextStep } = this.props; + onNextStep('comment'); + }; + + render () { + const { availableStatusIds, selectedStatusIds, onToggle } = this.props; + + return ( + <React.Fragment> + <h3 className='report-dialog-modal__title'><FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' /></h3> + <p className='report-dialog-modal__lead'><FormattedMessage id='report.statuses.subtitle' defaultMessage='Select all that apply' /></p> + + <div className='report-dialog-modal__statuses'> + {availableStatusIds.union(selectedStatusIds).map(statusId => ( + <StatusCheckBox + id={statusId} + key={statusId} + checked={selectedStatusIds.includes(statusId)} + onToggle={onToggle} + /> + ))} + </div> + + <div className='flex-spacer' /> + + <div className='report-dialog-modal__actions'> + <Button onClick={this.handleNextClick}><FormattedMessage id='report.next' defaultMessage='Next' /></Button> + </div> + </React.Fragment> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/thanks.js b/app/javascript/flavours/glitch/features/report/thanks.js new file mode 100644 index 000000000..9c41baa7f --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/thanks.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import { connect } from 'react-redux'; +import { + unfollowAccount, + muteAccount, + blockAccount, +} from 'mastodon/actions/accounts'; + +const mapStateToProps = () => ({}); + +export default @connect(mapStateToProps) +class Thanks extends React.PureComponent { + + static propTypes = { + submitted: PropTypes.bool, + onClose: PropTypes.func.isRequired, + account: ImmutablePropTypes.map.isRequired, + dispatch: PropTypes.func.isRequired, + }; + + handleCloseClick = () => { + const { onClose } = this.props; + onClose(); + }; + + handleUnfollowClick = () => { + const { dispatch, account, onClose } = this.props; + dispatch(unfollowAccount(account.get('id'))); + onClose(); + }; + + handleMuteClick = () => { + const { dispatch, account, onClose } = this.props; + dispatch(muteAccount(account.get('id'))); + onClose(); + }; + + handleBlockClick = () => { + const { dispatch, account, onClose } = this.props; + dispatch(blockAccount(account.get('id'))); + onClose(); + }; + + render () { + const { account, submitted } = this.props; + + return ( + <React.Fragment> + <h3 className='report-dialog-modal__title'>{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}</h3> + <p className='report-dialog-modal__lead'>{submitted ? <FormattedMessage id='report.thanks.take_action_actionable' defaultMessage='While we review this, you can take action against @{name}:' values={{ name: account.get('username') }} /> : <FormattedMessage id='report.thanks.take_action' defaultMessage='Here are your options for controlling what you see on Mastodon:' />}</p> + + {account.getIn(['relationship', 'following']) && ( + <React.Fragment> + <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='report.unfollow' defaultMessage='Unfollow @{name}' values={{ name: account.get('username') }} /></h4> + <p className='report-dialog-modal__lead'><FormattedMessage id='report.unfollow_explanation' defaultMessage='You are following this account. To not see their posts in your home feed anymore, unfollow them.' /></p> + <Button secondary onClick={this.handleUnfollowClick}><FormattedMessage id='account.unfollow' defaultMessage='Unfollow' /></Button> + <hr /> + </React.Fragment> + )} + + <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.mute' defaultMessage='Mute @{name}' values={{ name: account.get('username') }} /></h4> + <p className='report-dialog-modal__lead'><FormattedMessage id='report.mute_explanation' defaultMessage='You will not see their posts. They can still follow you and see your posts and will not know that they are muted.' /></p> + <Button secondary onClick={this.handleMuteClick}>{!account.getIn(['relationship', 'muting']) ? <FormattedMessage id='report.mute' defaultMessage='Mute' /> : <FormattedMessage id='account.muted' defaultMessage='Muted' />}</Button> + + <hr /> + + <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.block' defaultMessage='Block @{name}' values={{ name: account.get('username') }} /></h4> + <p className='report-dialog-modal__lead'><FormattedMessage id='report.block_explanation' defaultMessage='You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.' /></p> + <Button secondary onClick={this.handleBlockClick}>{!account.getIn(['relationship', 'blocking']) ? <FormattedMessage id='report.block' defaultMessage='Block' /> : <FormattedMessage id='account.blocked' defaultMessage='Blocked' />}</Button> + + <div className='flex-spacer' /> + + <div className='report-dialog-modal__actions'> + <Button onClick={this.handleCloseClick}><FormattedMessage id='report.close' defaultMessage='Done' /></Button> + </div> + </React.Fragment> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index 5cb7c5d07..dcbe94929 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -1,38 +1,32 @@ import React from 'react'; import { connect } from 'react-redux'; -import { changeReportComment, changeReportForward, submitReport } from 'flavours/glitch/actions/reports'; +import { submitReport } from 'flavours/glitch/actions/reports'; import { expandAccountTimeline } from 'flavours/glitch/actions/timelines'; +import { fetchRules } from 'flavours/glitch/actions/rules'; +import { fetchRelationships } from 'flavours/glitch/actions/accounts'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { makeGetAccount } from 'flavours/glitch/selectors'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container'; import { OrderedSet } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Button from 'flavours/glitch/components/button'; -import Toggle from 'react-toggle'; -import IconButton from '../../../components/icon_button'; +import IconButton from 'flavours/glitch/components/icon_button'; +import Category from 'flavours/glitch/features/report/category'; +import Statuses from 'flavours/glitch/features/report/statuses'; +import Rules from 'flavours/glitch/features/report/rules'; +import Comment from 'flavours/glitch/features/report/comment'; +import Thanks from 'flavours/glitch/features/report/thanks'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, - placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, - submit: { id: 'report.submit', defaultMessage: 'Submit' }, }); const makeMapStateToProps = () => { const getAccount = makeGetAccount(); - const mapStateToProps = state => { - const accountId = state.getIn(['reports', 'new', 'account_id']); - - return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), - account: getAccount(state, accountId), - comment: state.getIn(['reports', 'new', 'comment']), - forward: state.getIn(['reports', 'new', 'forward']), - statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), - }; - }; + const mapStateToProps = (state, { accountId }) => ({ + account: getAccount(state, accountId), + }); return mapStateToProps; }; @@ -42,92 +36,183 @@ export default @connect(makeMapStateToProps) class ReportModal extends ImmutablePureComponent { static propTypes = { - isSubmitting: PropTypes.bool, - account: ImmutablePropTypes.map, - statusIds: ImmutablePropTypes.orderedSet.isRequired, - comment: PropTypes.string.isRequired, - forward: PropTypes.bool, + accountId: PropTypes.string.isRequired, + statusId: PropTypes.string, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + account: ImmutablePropTypes.map.isRequired, }; - handleCommentChange = e => { - this.props.dispatch(changeReportComment(e.target.value)); - } - - handleForwardChange = e => { - this.props.dispatch(changeReportForward(e.target.checked)); - } + state = { + step: 'category', + selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []), + comment: '', + category: null, + selectedRuleIds: OrderedSet(), + forward: true, + isSubmitting: false, + isSubmitted: false, + }; handleSubmit = () => { - this.props.dispatch(submitReport()); - } + const { dispatch, accountId } = this.props; + const { selectedStatusIds, comment, category, selectedRuleIds, forward } = this.state; + + this.setState({ isSubmitting: true }); + + dispatch(submitReport({ + account_id: accountId, + status_ids: selectedStatusIds.toArray(), + comment, + forward, + category, + rule_ids: selectedRuleIds.toArray(), + }, this.handleSuccess, this.handleFail)); + }; + + handleSuccess = () => { + this.setState({ isSubmitting: false, isSubmitted: true, step: 'thanks' }); + }; + + handleFail = () => { + this.setState({ isSubmitting: false }); + }; + + handleStatusToggle = (statusId, checked) => { + const { selectedStatusIds } = this.state; + + if (checked) { + this.setState({ selectedStatusIds: selectedStatusIds.add(statusId) }); + } else { + this.setState({ selectedStatusIds: selectedStatusIds.remove(statusId) }); + } + }; - handleKeyDown = e => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); + handleRuleToggle = (ruleId, checked) => { + const { selectedRuleIds } = this.state; + + if (checked) { + this.setState({ selectedRuleIds: selectedRuleIds.add(ruleId) }); + } else { + this.setState({ selectedRuleIds: selectedRuleIds.remove(ruleId) }); } } + handleChangeCategory = category => { + this.setState({ category }); + }; + + handleChangeComment = comment => { + this.setState({ comment }); + }; + + handleChangeForward = forward => { + this.setState({ forward }); + }; + + handleNextStep = step => { + this.setState({ step }); + }; + componentDidMount () { - this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true })); - } + const { dispatch, accountId } = this.props; - componentWillReceiveProps (nextProps) { - if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true })); - } + dispatch(fetchRelationships([accountId])); + dispatch(expandAccountTimeline(accountId, { withReplies: true })); + dispatch(fetchRules()); } render () { - const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props; + const { + accountId, + account, + intl, + onClose, + } = this.props; if (!account) { return null; } - const domain = account.get('acct').split('@')[1]; + const { + step, + selectedStatusIds, + selectedRuleIds, + comment, + forward, + category, + isSubmitting, + isSubmitted, + } = this.state; + + const domain = account.get('acct').split('@')[1]; + const isRemote = !!domain; + + let stepComponent; + + switch(step) { + case 'category': + stepComponent = ( + <Category + onNextStep={this.handleNextStep} + startedFrom={this.props.statusId ? 'status' : 'account'} + category={category} + onChangeCategory={this.handleChangeCategory} + /> + ); + break; + case 'rules': + stepComponent = ( + <Rules + onNextStep={this.handleNextStep} + selectedRuleIds={selectedRuleIds} + onToggle={this.handleRuleToggle} + /> + ); + break; + case 'statuses': + stepComponent = ( + <Statuses + onNextStep={this.handleNextStep} + accountId={accountId} + selectedStatusIds={selectedStatusIds} + onToggle={this.handleStatusToggle} + /> + ); + break; + case 'comment': + stepComponent = ( + <Comment + onSubmit={this.handleSubmit} + isSubmitting={isSubmitting} + isRemote={isRemote} + comment={comment} + forward={forward} + domain={domain} + onChangeComment={this.handleChangeComment} + onChangeForward={this.handleChangeForward} + /> + ); + break; + case 'thanks': + stepComponent = ( + <Thanks + submitted={isSubmitted} + account={account} + onClose={onClose} + /> + ); + } return ( - <div className='modal-root__modal report-modal'> + <div className='modal-root__modal report-dialog-modal'> <div className='report-modal__target'> <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} /> <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} /> </div> - <div className='report-modal__container'> - <div className='report-modal__comment'> - <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p> - - <textarea - className='setting-text light' - placeholder={intl.formatMessage(messages.placeholder)} - value={comment} - onChange={this.handleCommentChange} - onKeyDown={this.handleKeyDown} - disabled={isSubmitting} - autoFocus - /> - - {domain && ( - <div> - <p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p> - - <div className='setting-toggle'> - <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} /> - <label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label> - </div> - </div> - )} - - <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /> - </div> - - <div className='report-modal__statuses'> - <div> - {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} - </div> - </div> + <div className='report-dialog-modal__container'> + {stepComponent} </div> </div> ); |