Change report modal to include category selection in web UI (#17565)

* Change report modal to include category selection in web UI

* Various fixes and improvements

- Change thank you text to be different based on category
- Change starting headline to be different for account and status reports
- Change toggle components to have a checkmark when checked
- Fix report dialog being cut off on small screens
- Fix thank you screen offering mute or block if already muted or blocked
- Refactor toggle components in report dialog into one component

* Change wording on final screen

* Change checkboxes to be square when multiple options are possible
This commit is contained in:
Eugen Rochko 2022-02-23 20:03:46 +01:00 committed by GitHub
parent 1c3e5e44e2
commit a9a43de6d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 954 additions and 232 deletions

View File

@ -1,89 +1,38 @@
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 const initReport = (account, status) => dispatch =>
dispatch(openModal('REPORT', {
accountId: account.get('id'),
statusId: status.get('id'),
}));
export function initReport(account, status) {
return dispatch => {
dispatch({
type: REPORT_INIT,
account,
status,
});
export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
dispatch(submitReportRequest());
dispatch(openModal('REPORT'));
};
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 function cancelReport() {
return {
type: REPORT_CANCEL,
};
};
export const submitReportRequest = () => ({
type: REPORT_SUBMIT_REQUEST,
});
export function toggleStatusReport(statusId, checked) {
return {
type: REPORT_STATUS_TOGGLE,
statusId,
checked,
};
};
export const submitReportSuccess = report => ({
type: REPORT_SUBMIT_SUCCESS,
report,
});
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 submitReportFail = error => ({
type: REPORT_SUBMIT_FAIL,
error,
});

View File

@ -0,0 +1,27 @@
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,
});

View File

@ -0,0 +1,9 @@
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;

View File

@ -0,0 +1,93 @@
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>
);
}
}

View File

@ -0,0 +1,83 @@
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>
);
}
}

View File

@ -0,0 +1,60 @@
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>
);
}
}

View File

@ -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 '../../../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')) {
@ -50,24 +59,46 @@ 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')} 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}
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><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div>
</div>
<div className='status-check-box-toggle'>
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />
</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
/>
);
}
}

View File

@ -1,19 +1,15 @@
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);

View File

@ -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 '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>
);
}
}

View File

@ -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 '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>
);
}
}

View File

@ -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 '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>
);
}
}

View File

@ -1,38 +1,31 @@
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;
};
@ -42,92 +35,182 @@ 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;
handleKeyDown = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
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) });
}
};
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>
);

View File

@ -17,7 +17,8 @@ import status_lists from './status_lists';
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';
@ -61,7 +62,8 @@ const reducers = {
mutes,
blocks,
boosts,
reports,
// reports,
rules,
contexts,
compose,
search,

View File

@ -0,0 +1,13 @@
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;
}
}

View File

@ -50,16 +50,14 @@
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;
@ -100,17 +98,6 @@
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;
@ -135,7 +122,7 @@
&.button-secondary {
color: $darker-text-color;
background: transparent;
padding: 3px 15px;
padding: 6px 17px;
border: 1px solid $ui-primary-color;
&:active,
@ -1114,42 +1101,39 @@
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;
}
.video-player,
.audio-player {
margin-top: 8px;
max-width: 250px;
}
.media-gallery__item-thumbnail {
cursor: default;
&:hover strong {
text-decoration: none;
}
}
}
.status-check-box-toggle {
align-items: center;
display: flex;
flex: 0 0 auto;
justify-content: center;
padding: 10px;
.media-gallery,
.audio-player,
.video-player {
margin-top: 8px;
max-width: 250px;
}
.status__content {
padding: 0;
white-space: normal;
}
.media-gallery__item-thumbnail {
cursor: default;
}
}
.status__prepend {
@ -5103,6 +5087,192 @@ a.status-card.compact:hover {
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;