import api from '../api';
-import { openModal, closeModal } from './modal';
-
-export const REPORT_INIT = 'REPORT_INIT';
-export const REPORT_CANCEL = 'REPORT_CANCEL';
+import { openModal } from './modal';
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
-export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
-export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
-export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
-
-export function initReport(account, status) {
- return dispatch => {
- dispatch({
- type: REPORT_INIT,
- account,
- status,
- });
-
- dispatch(openModal('REPORT'));
- };
-};
-
-export function cancelReport() {
- return {
- type: REPORT_CANCEL,
- };
-};
-
-export function toggleStatusReport(statusId, checked) {
- return {
- type: REPORT_STATUS_TOGGLE,
- statusId,
- checked,
- };
-};
-
-export function submitReport() {
- return (dispatch, getState) => {
- dispatch(submitReportRequest());
-
- api(getState).post('/api/v1/reports', {
- account_id: getState().getIn(['reports', 'new', 'account_id']),
- status_ids: getState().getIn(['reports', 'new', 'status_ids']),
- comment: getState().getIn(['reports', 'new', 'comment']),
- forward: getState().getIn(['reports', 'new', 'forward']),
- }).then(response => {
- dispatch(closeModal());
- dispatch(submitReportSuccess(response.data));
- }).catch(error => dispatch(submitReportFail(error)));
- };
-};
-
-export function submitReportRequest() {
- return {
- type: REPORT_SUBMIT_REQUEST,
- };
-};
-
-export function submitReportSuccess(report) {
- return {
- type: REPORT_SUBMIT_SUCCESS,
- report,
- };
-};
-
-export function submitReportFail(error) {
- return {
- type: REPORT_SUBMIT_FAIL,
- error,
- };
-};
-
-export function changeReportComment(comment) {
- return {
- type: REPORT_COMMENT_CHANGE,
- comment,
- };
-};
-
-export function changeReportForward(forward) {
- return {
- type: REPORT_FORWARD_CHANGE,
- forward,
- };
-};
+export const initReport = (account, status) => dispatch =>
+ dispatch(openModal('REPORT', {
+ accountId: account.get('id'),
+ statusId: status.get('id'),
+ }));
+
+export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
+ dispatch(submitReportRequest());
+
+ api(getState).post('/api/v1/reports', params).then(response => {
+ dispatch(submitReportSuccess(response.data));
+ if (onSuccess) onSuccess();
+ }).catch(error => {
+ dispatch(submitReportFail(error));
+ if (onFail) onFail();
+ });
+};
+
+export const submitReportRequest = () => ({
+ type: REPORT_SUBMIT_REQUEST,
+});
+
+export const submitReportSuccess = report => ({
+ type: REPORT_SUBMIT_SUCCESS,
+ report,
+});
+
+export const submitReportFail = error => ({
+ type: REPORT_SUBMIT_FAIL,
+ error,
+});
--- /dev/null
+import api from '../api';
+
+export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
+export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
+export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL';
+
+export const fetchRules = () => (dispatch, getState) => {
+ dispatch(fetchRulesRequest());
+
+ api(getState)
+ .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
+ .catch(err => dispatch(fetchRulesFail(err)));
+};
+
+const fetchRulesRequest = () => ({
+ type: RULES_FETCH_REQUEST,
+});
+
+const fetchRulesSuccess = rules => ({
+ type: RULES_FETCH_SUCCESS,
+ rules,
+});
+
+const fetchRulesFail = error => ({
+ type: RULES_FETCH_FAIL,
+ error,
+});
--- /dev/null
+import React from 'react';
+
+const Check = () => (
+ <svg width='14' height='11' viewBox='0 0 14 11'>
+ <path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' />
+ </svg>
+);
+
+export default Check;
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Button from 'mastodon/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+import Button from 'mastodon/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import Check from 'mastodon/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>
+ );
+ }
+
+}
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 '../../../components/status_content';
-import { MediaGallery, Video } from '../../ui/util/async-components';
-import Bundle from '../../ui/components/bundle';
+import StatusContent from 'mastodon/components/status_content';
+import { MediaGallery, Video } from 'mastodon/features/ui/util/async-components';
+import Bundle from 'mastodon/features/ui/components/bundle';
+import Avatar from 'mastodon/components/avatar';
+import DisplayName from 'mastodon/components/display_name';
+import RelativeTimestamp from 'mastodon/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')) {
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
- {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />}
+ {Component => (
+ <Component
+ media={status.get('media_attachments')}
+ sensitive={status.get('sensitive')}
+ height={110}
+ onOpenMedia={noop}
+ />
+ )}
</Bundle>
);
}
}
- return (
- <div className='status-check-box'>
- <div className='status-check-box__status'>
- <StatusContent status={status} />
- {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}
</div>
);
+
+ return (
+ <Option
+ name='status_ids'
+ value={status.get('id')}
+ checked={checked}
+ onToggle={this.handleStatusesToggle}
+ label={status.get('search_index')}
+ labelComponent={labelComponent}
+ multiple
+ />
+ );
}
}
import { connect } from 'react-redux';
import StatusCheckBox from '../components/status_check_box';
-import { toggleStatusReport } from '../../../actions/reports';
-import { Set as ImmutableSet } from 'immutable';
+import { makeGetStatus } from 'mastodon/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);
--- /dev/null
+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 'mastodon/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import StatusCheckBox from 'mastodon/features/report/containers/status_check_box_container';
+import { OrderedSet } from 'immutable';
+import { FormattedMessage } from 'react-intl';
+import Button from 'mastodon/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>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import Button from 'mastodon/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>
+ );
+ }
+
+}
import React from 'react';
import { connect } from 'react-redux';
-import { changeReportComment, changeReportForward, submitReport } from '../../../actions/reports';
-import { expandAccountTimeline } from '../../../actions/timelines';
+import { submitReport } from 'mastodon/actions/reports';
+import { expandAccountTimeline } from 'mastodon/actions/timelines';
+import { fetchRules } from 'mastodon/actions/rules';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { makeGetAccount } from '../../../selectors';
+import { makeGetAccount } from 'mastodon/selectors';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import StatusCheckBox from '../../report/containers/status_check_box_container';
import { OrderedSet } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Button from '../../../components/button';
-import Toggle from 'react-toggle';
-import IconButton from '../../../components/icon_button';
+import IconButton from 'mastodon/components/icon_button';
+import Category from 'mastodon/features/report/category';
+import Statuses from 'mastodon/features/report/statuses';
+import Rules from 'mastodon/features/report/rules';
+import Comment from 'mastodon/features/report/comment';
+import Thanks from 'mastodon/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;
};
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(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>
);
import mutes from './mutes';
import blocks from './blocks';
import boosts from './boosts';
-import reports from './reports';
+// import reports from './reports';
+import rules from './rules';
import contexts from './contexts';
import compose from './compose';
import search from './search';
mutes,
blocks,
boosts,
- reports,
+ // reports,
+ rules,
contexts,
compose,
search,
--- /dev/null
+import { RULES_FETCH_SUCCESS } from 'mastodon/actions/rules';
+import { List as ImmutableList, fromJS } from 'immutable';
+
+const initialState = ImmutableList();
+
+export default function rules(state = initialState, action) {
+ switch (action.type) {
+ case RULES_FETCH_SUCCESS:
+ return fromJS(action.rules);
+ default:
+ return state;
+ }
+}
cursor: pointer;
display: inline-block;
font-family: inherit;
- font-size: 14px;
+ font-size: 17px;
font-weight: 500;
- height: 36px;
letter-spacing: 0;
- line-height: 36px;
+ line-height: 22px;
overflow: hidden;
- padding: 0 16px;
+ padding: 7px 18px;
position: relative;
text-align: center;
- text-transform: uppercase;
text-decoration: none;
text-overflow: ellipsis;
transition: all 100ms ease-in;
outline: 0 !important;
}
- &.button-primary,
- &.button-alternative,
- &.button-secondary,
- &.button-alternative-2 {
- font-size: 16px;
- line-height: 36px;
- height: auto;
- text-transform: none;
- padding: 4px 16px;
- }
-
&.button-alternative {
color: $inverted-text-color;
background: $ui-primary-color;
&.button-secondary {
color: $darker-text-color;
background: transparent;
- padding: 3px 15px;
+ padding: 6px 17px;
border: 1px solid $ui-primary-color;
&:active,
font-size: 15px;
}
-.status-check-box {
- border-bottom: 1px solid $ui-secondary-color;
- display: flex;
+.status-check-box__status {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0 10px;
- .status-check-box__status {
- margin: 10px 0 10px 10px;
- flex: 1;
- overflow: hidden;
+ .detailed-status__display-name {
+ color: lighten($inverted-text-color, 16%);
- .media-gallery {
- max-width: 250px;
+ span {
+ display: inline;
}
- .status__content {
- padding: 0;
- white-space: normal;
+ &:hover strong {
+ text-decoration: none;
}
+ }
- .video-player,
- .audio-player {
- margin-top: 8px;
- max-width: 250px;
- }
+ .media-gallery,
+ .audio-player,
+ .video-player {
+ margin-top: 8px;
+ max-width: 250px;
+ }
- .media-gallery__item-thumbnail {
- cursor: default;
- }
+ .status__content {
+ padding: 0;
+ white-space: normal;
}
-}
-.status-check-box-toggle {
- align-items: center;
- display: flex;
- flex: 0 0 auto;
- justify-content: center;
- padding: 10px;
+ .media-gallery__item-thumbnail {
+ cursor: default;
+ }
}
.status__prepend {
max-width: 700px;
}
+.report-dialog-modal {
+ max-width: 90vw;
+ width: 480px;
+ height: 80vh;
+ background: lighten($ui-secondary-color, 8%);
+ color: $inverted-text-color;
+ border-radius: 8px;
+ overflow: hidden;
+ position: relative;
+ flex-direction: column;
+ display: flex;
+
+ &__container {
+ box-sizing: border-box;
+ border-top: 1px solid $ui-secondary-color;
+ padding: 20px;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: auto;
+ }
+
+ &__title {
+ font-size: 28px;
+ line-height: 33px;
+ font-weight: 700;
+ margin-bottom: 15px;
+
+ @media screen and (max-height: 800px) {
+ font-size: 22px;
+ }
+ }
+
+ &__subtitle {
+ font-size: 17px;
+ font-weight: 600;
+ line-height: 22px;
+ margin-bottom: 4px;
+ }
+
+ &__lead {
+ font-size: 17px;
+ line-height: 22px;
+ color: lighten($inverted-text-color, 16%);
+ margin-bottom: 30px;
+ }
+
+ &__actions {
+ margin-top: 30px;
+ display: flex;
+
+ .button {
+ flex: 1 1 auto;
+ }
+ }
+
+ &__statuses {
+ flex-grow: 1;
+ min-height: 0;
+ overflow: auto;
+ }
+
+ .status__content a {
+ color: $highlight-text-color;
+ }
+
+ .status__content,
+ .status__content p {
+ color: $inverted-text-color;
+ }
+
+ .dialog-option .poll__input {
+ border-color: $inverted-text-color;
+ color: $ui-secondary-color;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ width: 8px;
+ height: auto;
+ }
+
+ &:active,
+ &:focus,
+ &:hover {
+ border-color: lighten($inverted-text-color, 15%);
+ border-width: 4px;
+ }
+
+ &.active {
+ border-color: $inverted-text-color;
+ background: $inverted-text-color;
+ }
+ }
+
+ .poll__option.dialog-option {
+ padding: 15px 0;
+ flex: 0 0 auto;
+ border-bottom: 1px solid $ui-secondary-color;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ & > .poll__option__text {
+ font-size: 13px;
+ color: lighten($inverted-text-color, 16%);
+
+ strong {
+ font-size: 17px;
+ font-weight: 500;
+ line-height: 22px;
+ color: $inverted-text-color;
+ display: block;
+ margin-bottom: 4px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+
+ .flex-spacer {
+ background: transparent;
+ }
+
+ &__textarea {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ margin: 0;
+ color: $inverted-text-color;
+ background: $simple-background-color;
+ padding: 10px;
+ font-family: inherit;
+ font-size: 17px;
+ line-height: 22px;
+ resize: vertical;
+ border: 0;
+ outline: 0;
+ border-radius: 4px;
+ margin: 20px 0;
+
+ &::placeholder {
+ color: $dark-text-color;
+ }
+
+ &:focus {
+ outline: 0;
+ }
+ }
+
+ &__toggle {
+ display: flex;
+ align-items: center;
+
+ & > span {
+ font-size: 17px;
+ font-weight: 500;
+ margin-left: 10px;
+ }
+ }
+
+ .button.button-secondary {
+ border-color: $inverted-text-color;
+ color: $inverted-text-color;
+ flex: 0 0 auto;
+
+ &:hover,
+ &:focus,
+ &:active {
+ border-color: lighten($inverted-text-color, 15%);
+ color: lighten($inverted-text-color, 15%);
+ }
+ }
+
+ hr {
+ border: 0;
+ background: transparent;
+ margin: 15px 0;
+ }
+}
+
.report-modal__container {
display: flex;
border-top: 1px solid $ui-secondary-color;