mirror of
https://github.com/lunaisnotaboy/mastodon.git
synced 2024-11-02 13:04:37 +00:00
Merge pull request #1701 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
e48eaf64cc
|
@ -1,89 +1,38 @@
|
|||
import api from 'flavours/glitch/util/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,
|
||||
});
|
||||
|
|
27
app/javascript/flavours/glitch/actions/rules.js
Normal file
27
app/javascript/flavours/glitch/actions/rules.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import api from 'flavours/glitch/util/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/flavours/glitch/components/check.js
Normal file
9
app/javascript/flavours/glitch/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/flavours/glitch/features/report/category.js
Normal file
93
app/javascript/flavours/glitch/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 'flavours/glitch/components/button';
|
||||
import Option from './components/option';
|
||||
|
||||
const messages = defineMessages({
|
||||
dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
|
||||
dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
|
||||
spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
|
||||
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' },
|
||||
violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
|
||||
violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
|
||||
other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
|
||||
other_description: { id: 'report.reasons.other_description', defaultMessage: 'The issue does not fit into other categories' },
|
||||
status: { id: 'report.category.title_status', defaultMessage: 'post' },
|
||||
account: { id: 'report.category.title_account', defaultMessage: 'profile' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class Category extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onNextStep: PropTypes.func.isRequired,
|
||||
category: PropTypes.string,
|
||||
onChangeCategory: PropTypes.func.isRequired,
|
||||
startedFrom: PropTypes.oneOf(['status', 'account']),
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleNextClick = () => {
|
||||
const { onNextStep, category } = this.props;
|
||||
|
||||
switch(category) {
|
||||
case 'dislike':
|
||||
onNextStep('thanks');
|
||||
break;
|
||||
case 'violation':
|
||||
onNextStep('rules');
|
||||
break;
|
||||
default:
|
||||
onNextStep('statuses');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleCategoryToggle = (value, checked) => {
|
||||
const { onChangeCategory } = this.props;
|
||||
|
||||
if (checked) {
|
||||
onChangeCategory(value);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { category, startedFrom, intl } = this.props;
|
||||
|
||||
const options = [
|
||||
'dislike',
|
||||
'spam',
|
||||
'violation',
|
||||
'other',
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} /></h3>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.category.subtitle' defaultMessage='Choose the best match' /></p>
|
||||
|
||||
<div>
|
||||
{options.map(item => (
|
||||
<Option
|
||||
key={item}
|
||||
name='category'
|
||||
value={item}
|
||||
checked={category === item}
|
||||
onToggle={this.handleCategoryToggle}
|
||||
label={intl.formatMessage(messages[item])}
|
||||
description={intl.formatMessage(messages[`${item}_description`])}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='flex-spacer' />
|
||||
|
||||
<div className='report-dialog-modal__actions'>
|
||||
<Button onClick={this.handleNextClick} disabled={category === null}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
83
app/javascript/flavours/glitch/features/report/comment.js
Normal file
83
app/javascript/flavours/glitch/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 'flavours/glitch/components/button';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class Comment extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
comment: PropTypes.string.isRequired,
|
||||
onChangeComment: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isSubmitting: PropTypes.bool,
|
||||
forward: PropTypes.bool,
|
||||
isRemote: PropTypes.bool,
|
||||
domain: PropTypes.string,
|
||||
onChangeForward: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { onSubmit } = this.props;
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
const { onChangeComment } = this.props;
|
||||
onChangeComment(e.target.value);
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleClick();
|
||||
}
|
||||
};
|
||||
|
||||
handleForwardChange = e => {
|
||||
const { onChangeForward } = this.props;
|
||||
onChangeForward(e.target.checked);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3>
|
||||
|
||||
<textarea
|
||||
className='report-dialog-modal__textarea'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={comment}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
{isRemote && (
|
||||
<React.Fragment>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
|
||||
|
||||
<label className='report-dialog-modal__toggle'>
|
||||
<Toggle checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
|
||||
<FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
|
||||
</label>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<div className='flex-spacer' />
|
||||
|
||||
<div className='report-dialog-modal__actions'>
|
||||
<Button onClick={this.handleClick}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Check from 'flavours/glitch/components/check';
|
||||
|
||||
export default class Option extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
label: PropTypes.node,
|
||||
description: PropTypes.node,
|
||||
onToggle: PropTypes.func,
|
||||
multiple: PropTypes.bool,
|
||||
labelComponent: PropTypes.node,
|
||||
};
|
||||
|
||||
handleKeyPress = e => {
|
||||
const { value, checked, onToggle } = this.props;
|
||||
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onToggle(value, !checked);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
const { value, onToggle } = this.props;
|
||||
onToggle(value, e.target.checked);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, value, checked, label, labelComponent, description, multiple } = this.props;
|
||||
|
||||
return (
|
||||
<label className='dialog-option poll__option selectable'>
|
||||
<input type={multiple ? 'checkbox' : 'radio'} name={name} value={value} checked={checked} onChange={this.handleChange} />
|
||||
|
||||
<span
|
||||
className={classNames('poll__input', { active: checked, checkbox: multiple })}
|
||||
tabIndex='0'
|
||||
role='radio'
|
||||
onKeyPress={this.handleKeyPress}
|
||||
aria-checked={checked}
|
||||
aria-label={label}
|
||||
>{checked && <Check />}</span>
|
||||
|
||||
{labelComponent ? labelComponent : (
|
||||
<span className='poll__option__text'>
|
||||
<strong>{label}</strong>
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,23 +1,32 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Toggle from 'react-toggle';
|
||||
import noop from 'lodash/noop';
|
||||
import StatusContent from 'flavours/glitch/components/status_content';
|
||||
import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
|
||||
import Bundle from 'flavours/glitch/features/ui/components/bundle';
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import Option from './option';
|
||||
|
||||
export default class StatusCheckBox extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleStatusesToggle = (value, checked) => {
|
||||
const { onToggle } = this.props;
|
||||
onToggle(value, checked);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, checked, onToggle, disabled } = this.props;
|
||||
const { status, checked } = this.props;
|
||||
|
||||
let media = null;
|
||||
|
||||
if (status.get('reblog')) {
|
||||
|
@ -51,26 +60,45 @@ export default class StatusCheckBox extends React.PureComponent {
|
|||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} revealed={false} height={110} onOpenMedia={noop} />}
|
||||
{Component => (
|
||||
<Component
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
revealed={false}
|
||||
height={110}
|
||||
onOpenMedia={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='status-check-box'>
|
||||
<div className='status-check-box__status'>
|
||||
<StatusContent
|
||||
status={status}
|
||||
media={media}
|
||||
/>
|
||||
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={media} />
|
||||
</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 StatusCheckBox from '../components/status_check_box';
|
||||
import { toggleStatusReport } from 'flavours/glitch/actions/reports';
|
||||
import { Set as ImmutableSet } from 'immutable';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
status: state.getIn(['statuses', id]),
|
||||
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
|
||||
});
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
status: getStatus(state, { id }),
|
||||
});
|
||||
|
||||
onToggle (e) {
|
||||
dispatch(toggleStatusReport(id, e.target.checked));
|
||||
},
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
|
||||
export default connect(makeMapStateToProps)(StatusCheckBox);
|
||||
|
|
64
app/javascript/flavours/glitch/features/report/rules.js
Normal file
64
app/javascript/flavours/glitch/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 'flavours/glitch/components/button';
|
||||
import Option from './components/option';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
rules: state.get('rules'),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Rules extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onNextStep: PropTypes.func.isRequired,
|
||||
rules: ImmutablePropTypes.list,
|
||||
selectedRuleIds: ImmutablePropTypes.set.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleNextClick = () => {
|
||||
const { onNextStep } = this.props;
|
||||
onNextStep('statuses');
|
||||
};
|
||||
|
||||
handleRulesToggle = (value, checked) => {
|
||||
const { onToggle } = this.props;
|
||||
onToggle(value, checked);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { rules, selectedRuleIds } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' /></h3>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.rules.subtitle' defaultMessage='Select all that apply' /></p>
|
||||
|
||||
<div>
|
||||
{rules.map(item => (
|
||||
<Option
|
||||
key={item.get('id')}
|
||||
name='rule_ids'
|
||||
value={item.get('id')}
|
||||
checked={selectedRuleIds.includes(item.get('id'))}
|
||||
onToggle={this.handleRulesToggle}
|
||||
label={item.get('text')}
|
||||
multiple
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='flex-spacer' />
|
||||
|
||||
<div className='report-dialog-modal__actions'>
|
||||
<Button onClick={this.handleNextClick} disabled={selectedRuleIds.size < 1}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
58
app/javascript/flavours/glitch/features/report/statuses.js
Normal file
58
app/javascript/flavours/glitch/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 'flavours/glitch/features/report/containers/status_check_box_container';
|
||||
import { OrderedSet } from 'immutable';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from 'flavours/glitch/components/button';
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Statuses extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onNextStep: PropTypes.func.isRequired,
|
||||
accountId: PropTypes.string.isRequired,
|
||||
availableStatusIds: ImmutablePropTypes.set.isRequired,
|
||||
selectedStatusIds: ImmutablePropTypes.set.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleNextClick = () => {
|
||||
const { onNextStep } = this.props;
|
||||
onNextStep('comment');
|
||||
};
|
||||
|
||||
render () {
|
||||
const { availableStatusIds, selectedStatusIds, onToggle } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' /></h3>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.statuses.subtitle' defaultMessage='Select all that apply' /></p>
|
||||
|
||||
<div className='report-dialog-modal__statuses'>
|
||||
{availableStatusIds.union(selectedStatusIds).map(statusId => (
|
||||
<StatusCheckBox
|
||||
id={statusId}
|
||||
key={statusId}
|
||||
checked={selectedStatusIds.includes(statusId)}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='flex-spacer' />
|
||||
|
||||
<div className='report-dialog-modal__actions'>
|
||||
<Button onClick={this.handleNextClick}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
84
app/javascript/flavours/glitch/features/report/thanks.js
Normal file
84
app/javascript/flavours/glitch/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 'flavours/glitch/components/button';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
unfollowAccount,
|
||||
muteAccount,
|
||||
blockAccount,
|
||||
} from 'mastodon/actions/accounts';
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Thanks extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
submitted: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleCloseClick = () => {
|
||||
const { onClose } = this.props;
|
||||
onClose();
|
||||
};
|
||||
|
||||
handleUnfollowClick = () => {
|
||||
const { dispatch, account, onClose } = this.props;
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
onClose();
|
||||
};
|
||||
|
||||
handleMuteClick = () => {
|
||||
const { dispatch, account, onClose } = this.props;
|
||||
dispatch(muteAccount(account.get('id')));
|
||||
onClose();
|
||||
};
|
||||
|
||||
handleBlockClick = () => {
|
||||
const { dispatch, account, onClose } = this.props;
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
onClose();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, submitted } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h3 className='report-dialog-modal__title'>{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}</h3>
|
||||
<p className='report-dialog-modal__lead'>{submitted ? <FormattedMessage id='report.thanks.take_action_actionable' defaultMessage='While we review this, you can take action against @{name}:' values={{ name: account.get('username') }} /> : <FormattedMessage id='report.thanks.take_action' defaultMessage='Here are your options for controlling what you see on Mastodon:' />}</p>
|
||||
|
||||
{account.getIn(['relationship', 'following']) && (
|
||||
<React.Fragment>
|
||||
<h4 className='report-dialog-modal__subtitle'><FormattedMessage id='report.unfollow' defaultMessage='Unfollow @{name}' values={{ name: account.get('username') }} /></h4>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.unfollow_explanation' defaultMessage='You are following this account. To not see their posts in your home feed anymore, unfollow them.' /></p>
|
||||
<Button secondary onClick={this.handleUnfollowClick}><FormattedMessage id='account.unfollow' defaultMessage='Unfollow' /></Button>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.mute' defaultMessage='Mute @{name}' values={{ name: account.get('username') }} /></h4>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.mute_explanation' defaultMessage='You will not see their posts. They can still follow you and see your posts and will not know that they are muted.' /></p>
|
||||
<Button secondary onClick={this.handleMuteClick}>{!account.getIn(['relationship', 'muting']) ? <FormattedMessage id='report.mute' defaultMessage='Mute' /> : <FormattedMessage id='account.muted' defaultMessage='Muted' />}</Button>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.block' defaultMessage='Block @{name}' values={{ name: account.get('username') }} /></h4>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.block_explanation' defaultMessage='You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.' /></p>
|
||||
<Button secondary onClick={this.handleBlockClick}>{!account.getIn(['relationship', 'blocking']) ? <FormattedMessage id='report.block' defaultMessage='Block' /> : <FormattedMessage id='account.blocked' defaultMessage='Blocked' />}</Button>
|
||||
|
||||
<div className='flex-spacer' />
|
||||
|
||||
<div className='report-dialog-modal__actions'>
|
||||
<Button onClick={this.handleCloseClick}><FormattedMessage id='report.close' defaultMessage='Done' /></Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,38 +1,32 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { changeReportComment, changeReportForward, submitReport } from 'flavours/glitch/actions/reports';
|
||||
import { submitReport } from 'flavours/glitch/actions/reports';
|
||||
import { expandAccountTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import { fetchRules } from 'flavours/glitch/actions/rules';
|
||||
import { fetchRelationships } from 'flavours/glitch/actions/accounts';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container';
|
||||
import { OrderedSet } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Button from 'flavours/glitch/components/button';
|
||||
import Toggle from 'react-toggle';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import Category from 'flavours/glitch/features/report/category';
|
||||
import Statuses from 'flavours/glitch/features/report/statuses';
|
||||
import Rules from 'flavours/glitch/features/report/rules';
|
||||
import Comment from 'flavours/glitch/features/report/comment';
|
||||
import Thanks from 'flavours/glitch/features/report/thanks';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
||||
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const accountId = state.getIn(['reports', 'new', 'account_id']);
|
||||
|
||||
return {
|
||||
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
||||
account: getAccount(state, accountId),
|
||||
comment: state.getIn(['reports', 'new', 'comment']),
|
||||
forward: state.getIn(['reports', 'new', 'forward']),
|
||||
statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
|
||||
};
|
||||
};
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
@ -42,92 +36,183 @@ export default @connect(makeMapStateToProps)
|
|||
class ReportModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isSubmitting: PropTypes.bool,
|
||||
account: ImmutablePropTypes.map,
|
||||
statusIds: ImmutablePropTypes.orderedSet.isRequired,
|
||||
comment: PropTypes.string.isRequired,
|
||||
forward: PropTypes.bool,
|
||||
accountId: PropTypes.string.isRequired,
|
||||
statusId: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
handleCommentChange = e => {
|
||||
this.props.dispatch(changeReportComment(e.target.value));
|
||||
}
|
||||
|
||||
handleForwardChange = e => {
|
||||
this.props.dispatch(changeReportForward(e.target.checked));
|
||||
}
|
||||
state = {
|
||||
step: 'category',
|
||||
selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []),
|
||||
comment: '',
|
||||
category: null,
|
||||
selectedRuleIds: OrderedSet(),
|
||||
forward: true,
|
||||
isSubmitting: false,
|
||||
isSubmitted: false,
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.dispatch(submitReport());
|
||||
}
|
||||
const { dispatch, accountId } = this.props;
|
||||
const { selectedStatusIds, comment, category, selectedRuleIds, forward } = this.state;
|
||||
|
||||
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(fetchRelationships([accountId]));
|
||||
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
||||
dispatch(fetchRules());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props;
|
||||
const {
|
||||
accountId,
|
||||
account,
|
||||
intl,
|
||||
onClose,
|
||||
} = this.props;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const domain = account.get('acct').split('@')[1];
|
||||
const {
|
||||
step,
|
||||
selectedStatusIds,
|
||||
selectedRuleIds,
|
||||
comment,
|
||||
forward,
|
||||
category,
|
||||
isSubmitting,
|
||||
isSubmitted,
|
||||
} = this.state;
|
||||
|
||||
const domain = account.get('acct').split('@')[1];
|
||||
const isRemote = !!domain;
|
||||
|
||||
let stepComponent;
|
||||
|
||||
switch(step) {
|
||||
case 'category':
|
||||
stepComponent = (
|
||||
<Category
|
||||
onNextStep={this.handleNextStep}
|
||||
startedFrom={this.props.statusId ? 'status' : 'account'}
|
||||
category={category}
|
||||
onChangeCategory={this.handleChangeCategory}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'rules':
|
||||
stepComponent = (
|
||||
<Rules
|
||||
onNextStep={this.handleNextStep}
|
||||
selectedRuleIds={selectedRuleIds}
|
||||
onToggle={this.handleRuleToggle}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'statuses':
|
||||
stepComponent = (
|
||||
<Statuses
|
||||
onNextStep={this.handleNextStep}
|
||||
accountId={accountId}
|
||||
selectedStatusIds={selectedStatusIds}
|
||||
onToggle={this.handleStatusToggle}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'comment':
|
||||
stepComponent = (
|
||||
<Comment
|
||||
onSubmit={this.handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
isRemote={isRemote}
|
||||
comment={comment}
|
||||
forward={forward}
|
||||
domain={domain}
|
||||
onChangeComment={this.handleChangeComment}
|
||||
onChangeForward={this.handleChangeForward}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'thanks':
|
||||
stepComponent = (
|
||||
<Thanks
|
||||
submitted={isSubmitted}
|
||||
account={account}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal report-modal'>
|
||||
<div className='modal-root__modal report-dialog-modal'>
|
||||
<div className='report-modal__target'>
|
||||
<IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
|
||||
<FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
|
||||
</div>
|
||||
|
||||
<div className='report-modal__container'>
|
||||
<div className='report-modal__comment'>
|
||||
<p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p>
|
||||
|
||||
<textarea
|
||||
className='setting-text light'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={comment}
|
||||
onChange={this.handleCommentChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{domain && (
|
||||
<div>
|
||||
<p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
|
||||
<label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />
|
||||
</div>
|
||||
|
||||
<div className='report-modal__statuses'>
|
||||
<div>
|
||||
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='report-dialog-modal__container'>
|
||||
{stepComponent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ import push_notifications from './push_notifications';
|
|||
import status_lists from './status_lists';
|
||||
import mutes from './mutes';
|
||||
import blocks from './blocks';
|
||||
import reports from './reports';
|
||||
import rules from './rules';
|
||||
import boosts from './boosts';
|
||||
import contexts from './contexts';
|
||||
import compose from './compose';
|
||||
|
@ -64,7 +64,7 @@ const reducers = {
|
|||
push_notifications,
|
||||
mutes,
|
||||
blocks,
|
||||
reports,
|
||||
rules,
|
||||
boosts,
|
||||
contexts,
|
||||
compose,
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import {
|
||||
REPORT_INIT,
|
||||
REPORT_SUBMIT_REQUEST,
|
||||
REPORT_SUBMIT_SUCCESS,
|
||||
REPORT_SUBMIT_FAIL,
|
||||
REPORT_CANCEL,
|
||||
REPORT_STATUS_TOGGLE,
|
||||
REPORT_COMMENT_CHANGE,
|
||||
REPORT_FORWARD_CHANGE,
|
||||
} from 'flavours/glitch/actions/reports';
|
||||
import {
|
||||
TIMELINE_DELETE,
|
||||
} from 'flavours/glitch/actions/timelines';
|
||||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
new: ImmutableMap({
|
||||
isSubmitting: false,
|
||||
account_id: null,
|
||||
status_ids: ImmutableSet(),
|
||||
comment: '',
|
||||
forward: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const deleteStatus = (state, id, references) => {
|
||||
references.forEach(ref => {
|
||||
state = deleteStatus(state, ref[0], []);
|
||||
});
|
||||
|
||||
return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.remove(id));
|
||||
};
|
||||
|
||||
export default function reports(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case REPORT_INIT:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['new', 'isSubmitting'], false);
|
||||
map.setIn(['new', 'account_id'], action.account.get('id'));
|
||||
|
||||
if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
|
||||
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
|
||||
map.setIn(['new', 'comment'], '');
|
||||
} else if (action.status) {
|
||||
map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
|
||||
}
|
||||
});
|
||||
case REPORT_STATUS_TOGGLE:
|
||||
return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => {
|
||||
if (action.checked) {
|
||||
return set.add(action.statusId);
|
||||
}
|
||||
|
||||
return set.remove(action.statusId);
|
||||
});
|
||||
case REPORT_COMMENT_CHANGE:
|
||||
return state.setIn(['new', 'comment'], action.comment);
|
||||
case REPORT_FORWARD_CHANGE:
|
||||
return state.setIn(['new', 'forward'], action.forward);
|
||||
case REPORT_SUBMIT_REQUEST:
|
||||
return state.setIn(['new', 'isSubmitting'], true);
|
||||
case REPORT_SUBMIT_FAIL:
|
||||
return state.setIn(['new', 'isSubmitting'], false);
|
||||
case REPORT_CANCEL:
|
||||
case REPORT_SUBMIT_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['new', 'account_id'], null);
|
||||
map.setIn(['new', 'status_ids'], ImmutableSet());
|
||||
map.setIn(['new', 'comment'], '');
|
||||
map.setIn(['new', 'isSubmitting'], false);
|
||||
});
|
||||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.references);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
13
app/javascript/flavours/glitch/reducers/rules.js
Normal file
13
app/javascript/flavours/glitch/reducers/rules.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { RULES_FETCH_SUCCESS } from 'flavours/glitch/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;
|
||||
}
|
||||
}
|
|
@ -652,14 +652,14 @@
|
|||
& > .primary {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
padding: 7px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& > .side_arm {
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
padding: 0;
|
||||
padding: 7px 0;
|
||||
width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -41,16 +41,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;
|
||||
|
@ -82,17 +80,6 @@
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
&.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;
|
||||
|
@ -121,8 +108,7 @@
|
|||
color: $darker-text-color;
|
||||
text-transform: none;
|
||||
background: transparent;
|
||||
padding: 3px 15px;
|
||||
border-radius: 4px;
|
||||
padding: 6px 17px;
|
||||
border: 1px solid $ui-primary-color;
|
||||
|
||||
&:active,
|
||||
|
|
|
@ -537,6 +537,192 @@
|
|||
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;
|
||||
|
|
|
@ -517,42 +517,39 @@
|
|||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
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 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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
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 { 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>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ import status_lists from './status_lists';
|
|||
import mutes from './mutes';
|
||||
import blocks from './blocks';
|
||||
import boosts from './boosts';
|
||||
import reports from './reports';
|
||||
import rules from './rules';
|
||||
import contexts from './contexts';
|
||||
import compose from './compose';
|
||||
import search from './search';
|
||||
|
@ -61,7 +61,7 @@ const reducers = {
|
|||
mutes,
|
||||
blocks,
|
||||
boosts,
|
||||
reports,
|
||||
rules,
|
||||
contexts,
|
||||
compose,
|
||||
search,
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
import {
|
||||
REPORT_INIT,
|
||||
REPORT_SUBMIT_REQUEST,
|
||||
REPORT_SUBMIT_SUCCESS,
|
||||
REPORT_SUBMIT_FAIL,
|
||||
REPORT_CANCEL,
|
||||
REPORT_STATUS_TOGGLE,
|
||||
REPORT_COMMENT_CHANGE,
|
||||
REPORT_FORWARD_CHANGE,
|
||||
} from '../actions/reports';
|
||||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
new: ImmutableMap({
|
||||
isSubmitting: false,
|
||||
account_id: null,
|
||||
status_ids: ImmutableSet(),
|
||||
comment: '',
|
||||
forward: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export default function reports(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case REPORT_INIT:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['new', 'isSubmitting'], false);
|
||||
map.setIn(['new', 'account_id'], action.account.get('id'));
|
||||
|
||||
if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
|
||||
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
|
||||
map.setIn(['new', 'comment'], '');
|
||||
} else if (action.status) {
|
||||
map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
|
||||
}
|
||||
});
|
||||
case REPORT_STATUS_TOGGLE:
|
||||
return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => {
|
||||
if (action.checked) {
|
||||
return set.add(action.statusId);
|
||||
}
|
||||
|
||||
return set.remove(action.statusId);
|
||||
});
|
||||
case REPORT_COMMENT_CHANGE:
|
||||
return state.setIn(['new', 'comment'], action.comment);
|
||||
case REPORT_FORWARD_CHANGE:
|
||||
return state.setIn(['new', 'forward'], action.forward);
|
||||
case REPORT_SUBMIT_REQUEST:
|
||||
return state.setIn(['new', 'isSubmitting'], true);
|
||||
case REPORT_SUBMIT_FAIL:
|
||||
return state.setIn(['new', 'isSubmitting'], false);
|
||||
case REPORT_CANCEL:
|
||||
case REPORT_SUBMIT_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['new', 'account_id'], null);
|
||||
map.setIn(['new', 'status_ids'], ImmutableSet());
|
||||
map.setIn(['new', 'comment'], '');
|
||||
map.setIn(['new', 'isSubmitting'], false);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
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;
|
||||
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;
|
||||
|
|
Loading…
Reference in a new issue