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:
parent
1c3e5e44e2
commit
a9a43de6d1
15 changed files with 954 additions and 232 deletions
|
@ -1,89 +1,38 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { openModal, closeModal } from './modal';
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const REPORT_INIT = 'REPORT_INIT';
|
|
||||||
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
|
||||||
|
|
||||||
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
|
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
|
||||||
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
|
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
|
||||||
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
|
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
|
||||||
|
|
||||||
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
|
export const initReport = (account, status) => dispatch =>
|
||||||
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
dispatch(openModal('REPORT', {
|
||||||
export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
|
accountId: account.get('id'),
|
||||||
|
statusId: status.get('id'),
|
||||||
|
}));
|
||||||
|
|
||||||
export function initReport(account, status) {
|
export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||||
return dispatch => {
|
dispatch(submitReportRequest());
|
||||||
dispatch({
|
|
||||||
type: REPORT_INIT,
|
|
||||||
account,
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
|
|
||||||
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() {
|
export const submitReportRequest = () => ({
|
||||||
return {
|
type: REPORT_SUBMIT_REQUEST,
|
||||||
type: REPORT_CANCEL,
|
});
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function toggleStatusReport(statusId, checked) {
|
export const submitReportSuccess = report => ({
|
||||||
return {
|
type: REPORT_SUBMIT_SUCCESS,
|
||||||
type: REPORT_STATUS_TOGGLE,
|
report,
|
||||||
statusId,
|
});
|
||||||
checked,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function submitReport() {
|
export const submitReportFail = error => ({
|
||||||
return (dispatch, getState) => {
|
type: REPORT_SUBMIT_FAIL,
|
||||||
dispatch(submitReportRequest());
|
error,
|
||||||
|
});
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
27
app/javascript/mastodon/actions/rules.js
Normal file
27
app/javascript/mastodon/actions/rules.js
Normal 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,
|
||||||
|
});
|
9
app/javascript/mastodon/components/check.js
Normal file
9
app/javascript/mastodon/components/check.js
Normal 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;
|
93
app/javascript/mastodon/features/report/category.js
Normal file
93
app/javascript/mastodon/features/report/category.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
83
app/javascript/mastodon/features/report/comment.js
Normal file
83
app/javascript/mastodon/features/report/comment.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
app/javascript/mastodon/features/report/components/option.js
Normal file
60
app/javascript/mastodon/features/report/components/option.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,23 +1,32 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
import StatusContent from '../../../components/status_content';
|
import StatusContent from 'mastodon/components/status_content';
|
||||||
import { MediaGallery, Video } from '../../ui/util/async-components';
|
import { MediaGallery, Video } from 'mastodon/features/ui/util/async-components';
|
||||||
import Bundle from '../../ui/components/bundle';
|
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 {
|
export default class StatusCheckBox extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
checked: PropTypes.bool,
|
checked: PropTypes.bool,
|
||||||
onToggle: PropTypes.func.isRequired,
|
onToggle: PropTypes.func.isRequired,
|
||||||
disabled: PropTypes.bool,
|
};
|
||||||
|
|
||||||
|
handleStatusesToggle = (value, checked) => {
|
||||||
|
const { onToggle } = this.props;
|
||||||
|
onToggle(value, checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, checked, onToggle, disabled } = this.props;
|
const { status, checked } = this.props;
|
||||||
|
|
||||||
let media = null;
|
let media = null;
|
||||||
|
|
||||||
if (status.get('reblog')) {
|
if (status.get('reblog')) {
|
||||||
|
@ -50,24 +59,46 @@ export default class StatusCheckBox extends React.PureComponent {
|
||||||
} else {
|
} else {
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
<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>
|
</Bundle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const labelComponent = (
|
||||||
<div className='status-check-box'>
|
<div className='status-check-box__status poll__option__text'>
|
||||||
<div className='status-check-box__status'>
|
<div className='detailed-status__display-name'>
|
||||||
<StatusContent status={status} />
|
<div className='detailed-status__display-avatar'>
|
||||||
{media}
|
<Avatar account={status.get('account')} size={46} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='status-check-box-toggle'>
|
<StatusContent status={status} />
|
||||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />
|
|
||||||
</div>
|
{media}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Option
|
||||||
|
name='status_ids'
|
||||||
|
value={status.get('id')}
|
||||||
|
checked={checked}
|
||||||
|
onToggle={this.handleStatusesToggle}
|
||||||
|
label={status.get('search_index')}
|
||||||
|
labelComponent={labelComponent}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import StatusCheckBox from '../components/status_check_box';
|
import StatusCheckBox from '../components/status_check_box';
|
||||||
import { toggleStatusReport } from '../../../actions/reports';
|
import { makeGetStatus } from 'mastodon/selectors';
|
||||||
import { Set as ImmutableSet } from 'immutable';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const makeMapStateToProps = () => {
|
||||||
status: state.getIn(['statuses', id]),
|
const getStatus = makeGetStatus();
|
||||||
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
status: getStatus(state, { id }),
|
||||||
|
});
|
||||||
|
|
||||||
onToggle (e) {
|
return mapStateToProps;
|
||||||
dispatch(toggleStatusReport(id, e.target.checked));
|
};
|
||||||
},
|
|
||||||
|
|
||||||
});
|
export default connect(makeMapStateToProps)(StatusCheckBox);
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
|
|
||||||
|
|
64
app/javascript/mastodon/features/report/rules.js
Normal file
64
app/javascript/mastodon/features/report/rules.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
58
app/javascript/mastodon/features/report/statuses.js
Normal file
58
app/javascript/mastodon/features/report/statuses.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
84
app/javascript/mastodon/features/report/thanks.js
Normal file
84
app/javascript/mastodon/features/report/thanks.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,38 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { changeReportComment, changeReportForward, submitReport } from '../../../actions/reports';
|
import { submitReport } from 'mastodon/actions/reports';
|
||||||
import { expandAccountTimeline } from '../../../actions/timelines';
|
import { expandAccountTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import { fetchRules } from 'mastodon/actions/rules';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { makeGetAccount } from '../../../selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import StatusCheckBox from '../../report/containers/status_check_box_container';
|
|
||||||
import { OrderedSet } from 'immutable';
|
import { OrderedSet } from 'immutable';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Button from '../../../components/button';
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import Toggle from 'react-toggle';
|
import Category from 'mastodon/features/report/category';
|
||||||
import IconButton from '../../../components/icon_button';
|
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({
|
const messages = defineMessages({
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
|
||||||
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
const accountId = state.getIn(['reports', 'new', 'account_id']);
|
account: getAccount(state, accountId),
|
||||||
|
});
|
||||||
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'])),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
@ -42,92 +35,182 @@ export default @connect(makeMapStateToProps)
|
||||||
class ReportModal extends ImmutablePureComponent {
|
class ReportModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isSubmitting: PropTypes.bool,
|
accountId: PropTypes.string.isRequired,
|
||||||
account: ImmutablePropTypes.map,
|
statusId: PropTypes.string,
|
||||||
statusIds: ImmutablePropTypes.orderedSet.isRequired,
|
|
||||||
comment: PropTypes.string.isRequired,
|
|
||||||
forward: PropTypes.bool,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCommentChange = e => {
|
state = {
|
||||||
this.props.dispatch(changeReportComment(e.target.value));
|
step: 'category',
|
||||||
}
|
selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []),
|
||||||
|
comment: '',
|
||||||
handleForwardChange = e => {
|
category: null,
|
||||||
this.props.dispatch(changeReportForward(e.target.checked));
|
selectedRuleIds: OrderedSet(),
|
||||||
}
|
forward: true,
|
||||||
|
isSubmitting: false,
|
||||||
|
isSubmitted: false,
|
||||||
|
};
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
this.props.dispatch(submitReport());
|
const { dispatch, accountId } = this.props;
|
||||||
}
|
const { selectedStatusIds, comment, category, selectedRuleIds, forward } = this.state;
|
||||||
|
|
||||||
handleKeyDown = e => {
|
this.setState({ isSubmitting: true });
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
|
||||||
this.handleSubmit();
|
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 () {
|
componentDidMount () {
|
||||||
this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true }));
|
const { dispatch, accountId } = this.props;
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
||||||
if (this.props.account !== nextProps.account && nextProps.account) {
|
dispatch(fetchRules());
|
||||||
this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props;
|
const {
|
||||||
|
accountId,
|
||||||
|
account,
|
||||||
|
intl,
|
||||||
|
onClose,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<div className='modal-root__modal report-modal'>
|
<div className='modal-root__modal report-dialog-modal'>
|
||||||
<div className='report-modal__target'>
|
<div className='report-modal__target'>
|
||||||
<IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
|
<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> }} />
|
<FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='report-modal__container'>
|
<div className='report-dialog-modal__container'>
|
||||||
<div className='report-modal__comment'>
|
{stepComponent}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,8 @@ import status_lists from './status_lists';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import boosts from './boosts';
|
import boosts from './boosts';
|
||||||
import reports from './reports';
|
// import reports from './reports';
|
||||||
|
import rules from './rules';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import compose from './compose';
|
import compose from './compose';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
|
@ -61,7 +62,8 @@ const reducers = {
|
||||||
mutes,
|
mutes,
|
||||||
blocks,
|
blocks,
|
||||||
boosts,
|
boosts,
|
||||||
reports,
|
// reports,
|
||||||
|
rules,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
compose,
|
||||||
search,
|
search,
|
||||||
|
|
13
app/javascript/mastodon/reducers/rules.js
Normal file
13
app/javascript/mastodon/reducers/rules.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,16 +50,14 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 14px;
|
font-size: 17px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
height: 36px;
|
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
line-height: 36px;
|
line-height: 22px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 16px;
|
padding: 7px 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: uppercase;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
transition: all 100ms ease-in;
|
transition: all 100ms ease-in;
|
||||||
|
@ -100,17 +98,6 @@
|
||||||
outline: 0 !important;
|
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 {
|
&.button-alternative {
|
||||||
color: $inverted-text-color;
|
color: $inverted-text-color;
|
||||||
background: $ui-primary-color;
|
background: $ui-primary-color;
|
||||||
|
@ -135,7 +122,7 @@
|
||||||
&.button-secondary {
|
&.button-secondary {
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 3px 15px;
|
padding: 6px 17px;
|
||||||
border: 1px solid $ui-primary-color;
|
border: 1px solid $ui-primary-color;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
|
@ -1114,42 +1101,39 @@
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-check-box {
|
.status-check-box__status {
|
||||||
border-bottom: 1px solid $ui-secondary-color;
|
display: block;
|
||||||
display: flex;
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
.status-check-box__status {
|
.detailed-status__display-name {
|
||||||
margin: 10px 0 10px 10px;
|
color: lighten($inverted-text-color, 16%);
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.media-gallery {
|
span {
|
||||||
max-width: 250px;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__content {
|
&:hover strong {
|
||||||
padding: 0;
|
text-decoration: none;
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-player,
|
|
||||||
.audio-player {
|
|
||||||
margin-top: 8px;
|
|
||||||
max-width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-gallery__item-thumbnail {
|
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.status-check-box-toggle {
|
.media-gallery,
|
||||||
align-items: center;
|
.audio-player,
|
||||||
display: flex;
|
.video-player {
|
||||||
flex: 0 0 auto;
|
margin-top: 8px;
|
||||||
justify-content: center;
|
max-width: 250px;
|
||||||
padding: 10px;
|
}
|
||||||
|
|
||||||
|
.status__content {
|
||||||
|
padding: 0;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-gallery__item-thumbnail {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__prepend {
|
.status__prepend {
|
||||||
|
@ -5103,6 +5087,192 @@ a.status-card.compact:hover {
|
||||||
max-width: 700px;
|
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 {
|
.report-modal__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-top: 1px solid $ui-secondary-color;
|
border-top: 1px solid $ui-secondary-color;
|
||||||
|
|
Loading…
Reference in a new issue