Adding POST /api/v1/reports API, and a UI for submitting reports
parent
40a4053732
commit
3b81baaaaf
|
@ -1,4 +1,4 @@
|
||||||
import api from '../api'
|
import api from '../api';
|
||||||
|
|
||||||
import { updateTimeline } from './timelines';
|
import { updateTimeline } from './timelines';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const REPORT_INIT = 'REPORT_INIT';
|
||||||
|
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||||
|
|
||||||
|
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 function initReport(account, status) {
|
||||||
|
return {
|
||||||
|
type: REPORT_INIT,
|
||||||
|
account,
|
||||||
|
status
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function cancelReport() {
|
||||||
|
return {
|
||||||
|
type: REPORT_CANCEL
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toggleStatusReport(statusId, checked) {
|
||||||
|
return {
|
||||||
|
type: REPORT_STATUS_TOGGLE,
|
||||||
|
statusId,
|
||||||
|
checked,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function submitReport() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(submitReportRequest());
|
||||||
|
|
||||||
|
api(getState).post('/api/v1/reports', {
|
||||||
|
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
||||||
|
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
||||||
|
comment: getState().getIn(['reports', 'new', 'comment'])
|
||||||
|
}).then(response => 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
|
||||||
|
};
|
||||||
|
};
|
|
@ -11,7 +11,8 @@ const messages = defineMessages({
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
open: { id: 'status.open', defaultMessage: 'Expand' }
|
open: { id: 'status.open', defaultMessage: 'Expand' },
|
||||||
|
report: { id: 'status.report', defaultMessage: 'Report' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const StatusActionBar = React.createClass({
|
const StatusActionBar = React.createClass({
|
||||||
|
@ -27,7 +28,10 @@ const StatusActionBar = React.createClass({
|
||||||
onReblog: React.PropTypes.func,
|
onReblog: React.PropTypes.func,
|
||||||
onDelete: React.PropTypes.func,
|
onDelete: React.PropTypes.func,
|
||||||
onMention: React.PropTypes.func,
|
onMention: React.PropTypes.func,
|
||||||
onBlock: React.PropTypes.func
|
onBlock: React.PropTypes.func,
|
||||||
|
onReport: React.PropTypes.func,
|
||||||
|
me: React.PropTypes.number.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
@ -60,6 +64,11 @@ const StatusActionBar = React.createClass({
|
||||||
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
|
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleReport () {
|
||||||
|
this.props.onReport(this.props.status);
|
||||||
|
this.context.router.push('/report');
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, me, intl } = this.props;
|
const { status, me, intl } = this.props;
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
@ -71,6 +80,7 @@ const StatusActionBar = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
|
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.block), action: this.handleBlockClick });
|
menu.push({ text: intl.formatMessage(messages.block), action: this.handleBlockClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.report), action: this.handleReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -34,6 +34,7 @@ import FollowRequests from '../features/follow_requests';
|
||||||
import GenericNotFound from '../features/generic_not_found';
|
import GenericNotFound from '../features/generic_not_found';
|
||||||
import FavouritedStatuses from '../features/favourited_statuses';
|
import FavouritedStatuses from '../features/favourited_statuses';
|
||||||
import Blocks from '../features/blocks';
|
import Blocks from '../features/blocks';
|
||||||
|
import Report from '../features/report';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import en from 'react-intl/locale-data/en';
|
import en from 'react-intl/locale-data/en';
|
||||||
import de from 'react-intl/locale-data/de';
|
import de from 'react-intl/locale-data/de';
|
||||||
|
@ -131,6 +132,7 @@ const Mastodon = React.createClass({
|
||||||
|
|
||||||
<Route path='follow_requests' component={FollowRequests} />
|
<Route path='follow_requests' component={FollowRequests} />
|
||||||
<Route path='blocks' component={Blocks} />
|
<Route path='blocks' component={Blocks} />
|
||||||
|
<Route path='report' component={Report} />
|
||||||
|
|
||||||
<Route path='*' component={GenericNotFound} />
|
<Route path='*' component={GenericNotFound} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import { blockAccount } from '../actions/accounts';
|
import { blockAccount } from '../actions/accounts';
|
||||||
import { deleteStatus } from '../actions/statuses';
|
import { deleteStatus } from '../actions/statuses';
|
||||||
|
import { initReport } from '../actions/reports';
|
||||||
import { openMedia } from '../actions/modal';
|
import { openMedia } from '../actions/modal';
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
import { isMobile } from '../is_mobile'
|
import { isMobile } from '../is_mobile'
|
||||||
|
@ -97,6 +98,10 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
onBlock (account) {
|
onBlock (account) {
|
||||||
dispatch(blockAccount(account.get('id')));
|
dispatch(blockAccount(account.get('id')));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReport (status) {
|
||||||
|
dispatch(initReport(status.get('account'), status));
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,8 @@ const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
block: { id: 'account.block', defaultMessage: 'Block' },
|
block: { id: 'account.block', defaultMessage: 'Block' },
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
block: { id: 'account.block', defaultMessage: 'Block' }
|
block: { id: 'account.block', defaultMessage: 'Block' },
|
||||||
|
report: { id: 'account.report', defaultMessage: 'Report' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const outerDropdownStyle = {
|
const outerDropdownStyle = {
|
||||||
|
@ -32,7 +33,9 @@ const ActionBar = React.createClass({
|
||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func,
|
onFollow: React.PropTypes.func,
|
||||||
onBlock: React.PropTypes.func.isRequired,
|
onBlock: React.PropTypes.func.isRequired,
|
||||||
onMention: React.PropTypes.func.isRequired
|
onMention: React.PropTypes.func.isRequired,
|
||||||
|
onReport: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
@ -54,6 +57,10 @@ const ActionBar = React.createClass({
|
||||||
menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
|
menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.get('id') !== me) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.report), action: this.props.onReport });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__action-bar'>
|
<div className='account__action-bar'>
|
||||||
<div style={outerDropdownStyle}>
|
<div style={outerDropdownStyle}>
|
||||||
|
|
|
@ -13,7 +13,8 @@ const Header = React.createClass({
|
||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func.isRequired,
|
onFollow: React.PropTypes.func.isRequired,
|
||||||
onBlock: React.PropTypes.func.isRequired,
|
onBlock: React.PropTypes.func.isRequired,
|
||||||
onMention: React.PropTypes.func.isRequired
|
onMention: React.PropTypes.func.isRequired,
|
||||||
|
onReport: React.PropTypes.func.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
@ -30,6 +31,11 @@ const Header = React.createClass({
|
||||||
this.props.onMention(this.props.account, this.context.router);
|
this.props.onMention(this.props.account, this.context.router);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleReport () {
|
||||||
|
this.props.onReport(this.props.account);
|
||||||
|
this.context.router.push('/report');
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me } = this.props;
|
const { account, me } = this.props;
|
||||||
|
|
||||||
|
@ -50,6 +56,7 @@ const Header = React.createClass({
|
||||||
me={me}
|
me={me}
|
||||||
onBlock={this.handleBlock}
|
onBlock={this.handleBlock}
|
||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
|
onReport={this.handleReport}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
unblockAccount
|
unblockAccount
|
||||||
} from '../../../actions/accounts';
|
} from '../../../actions/accounts';
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
import { mentionCompose } from '../../../actions/compose';
|
||||||
|
import { initReport } from '../../../actions/reports';
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
@ -39,6 +40,10 @@ const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
onMention (account, router) {
|
onMention (account, router) {
|
||||||
dispatch(mentionCompose(account, router));
|
dispatch(mentionCompose(account, router));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReport (account) {
|
||||||
|
dispatch(initReport(account));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import emojify from '../../../emoji';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
const StatusCheckBox = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
|
checked: React.PropTypes.bool,
|
||||||
|
onToggle: React.PropTypes.func.isRequired,
|
||||||
|
disabled: React.PropTypes.bool
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { status, checked, onToggle, disabled } = this.props;
|
||||||
|
const content = { __html: emojify(status.get('content')) };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='status-check-box' style={{ display: 'flex' }}>
|
||||||
|
<div
|
||||||
|
className='status__content'
|
||||||
|
style={{ flex: '1 1 auto', padding: '10px' }}
|
||||||
|
dangerouslySetInnerHTML={content}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ flex: '0 0 auto', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default StatusCheckBox;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import StatusCheckBox from '../components/status_check_box';
|
||||||
|
import { toggleStatusReport } from '../../../actions/reports';
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
status: state.getIn(['statuses', id]),
|
||||||
|
checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id)
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||||
|
|
||||||
|
onToggle (e) {
|
||||||
|
dispatch(toggleStatusReport(id, e.target.checked));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { cancelReport, changeReportComment, submitReport } from '../../actions/reports';
|
||||||
|
import { fetchAccountTimeline } from '../../actions/accounts';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
import Button from '../../components/button';
|
||||||
|
import { makeGetAccount } from '../../selectors';
|
||||||
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
import StatusCheckBox from './containers/status_check_box_container';
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'report.heading', defaultMessage: 'New report' },
|
||||||
|
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']),
|
||||||
|
statusIds: state.getIn(['timelines', 'accounts_timelines', accountId, 'items'], Immutable.List())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const textareaStyle = {
|
||||||
|
marginBottom: '10px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const Report = React.createClass({
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
router: React.PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
isSubmitting: React.PropTypes.bool,
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
statusIds: ImmutablePropTypes.list.isRequired,
|
||||||
|
comment: React.PropTypes.string.isRequired,
|
||||||
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
if (!this.props.account) {
|
||||||
|
this.context.router.replace('/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
if (!this.props.account) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.dispatch(fetchAccountTimeline(this.props.account.get('id')));
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.props.account !== nextProps.account && nextProps.account) {
|
||||||
|
this.props.dispatch(fetchAccountTimeline(nextProps.account.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCommentChange (e) {
|
||||||
|
this.props.dispatch(changeReportComment(e.target.value));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSubmit () {
|
||||||
|
this.props.dispatch(submitReport());
|
||||||
|
this.context.router.replace('/');
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, comment, intl, statusIds, isSubmitting } = this.props;
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column heading={intl.formatMessage(messages.heading)} icon='flag'>
|
||||||
|
<ColumnBackButtonSlim />
|
||||||
|
<div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
|
||||||
|
<div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}>
|
||||||
|
<FormattedMessage id='report.target' defaultMessage='Reporting' />
|
||||||
|
<strong>{account.get('acct')}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: '1 1 auto' }} className='scrollable'>
|
||||||
|
<div>
|
||||||
|
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: '0 0 160px', padding: '10px' }}>
|
||||||
|
<textarea
|
||||||
|
className='report__textarea'
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
value={comment}
|
||||||
|
onChange={this.handleCommentChange}
|
||||||
|
style={textareaStyle}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||||
|
<div style={{ float: 'right' }}><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps)(injectIntl(Report));
|
|
@ -9,7 +9,8 @@ const messages = defineMessages({
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention' },
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
|
report: { id: 'status.report', defaultMessage: 'Report' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const ActionBar = React.createClass({
|
const ActionBar = React.createClass({
|
||||||
|
@ -25,6 +26,7 @@ const ActionBar = React.createClass({
|
||||||
onFavourite: React.PropTypes.func.isRequired,
|
onFavourite: React.PropTypes.func.isRequired,
|
||||||
onDelete: React.PropTypes.func.isRequired,
|
onDelete: React.PropTypes.func.isRequired,
|
||||||
onMention: React.PropTypes.func.isRequired,
|
onMention: React.PropTypes.func.isRequired,
|
||||||
|
onReport: React.PropTypes.func,
|
||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
@ -51,6 +53,11 @@ const ActionBar = React.createClass({
|
||||||
this.props.onMention(this.props.status.get('account'), this.context.router);
|
this.props.onMention(this.props.status.get('account'), this.context.router);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleReport () {
|
||||||
|
this.props.onReport(this.props.status);
|
||||||
|
this.context.router.push('/report');
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, me, intl } = this.props;
|
const { status, me, intl } = this.props;
|
||||||
|
|
||||||
|
@ -60,6 +67,7 @@ const ActionBar = React.createClass({
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
} else {
|
} else {
|
||||||
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
|
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.report), action: this.handleReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
mentionCompose
|
mentionCompose
|
||||||
} from '../../actions/compose';
|
} from '../../actions/compose';
|
||||||
import { deleteStatus } from '../../actions/statuses';
|
import { deleteStatus } from '../../actions/statuses';
|
||||||
|
import { initReport } from '../../actions/reports';
|
||||||
import {
|
import {
|
||||||
makeGetStatus,
|
makeGetStatus,
|
||||||
getStatusAncestors,
|
getStatusAncestors,
|
||||||
|
@ -88,6 +89,10 @@ const Status = React.createClass({
|
||||||
this.props.dispatch(openMedia(media, index));
|
this.props.dispatch(openMedia(media, index));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleReport (status) {
|
||||||
|
this.props.dispatch(initReport(status.get('account'), status));
|
||||||
|
},
|
||||||
|
|
||||||
renderChildren (list) {
|
renderChildren (list) {
|
||||||
return list.map(id => <StatusContainer key={id} id={id} />);
|
return list.map(id => <StatusContainer key={id} id={id} />);
|
||||||
},
|
},
|
||||||
|
@ -123,7 +128,7 @@ const Status = React.createClass({
|
||||||
{ancestors}
|
{ancestors}
|
||||||
|
|
||||||
<DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} />
|
<DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} />
|
||||||
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} />
|
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} />
|
||||||
|
|
||||||
{descendants}
|
{descendants}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import notifications from './notifications';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import cards from './cards';
|
import cards from './cards';
|
||||||
|
import reports from './reports';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
timelines,
|
timelines,
|
||||||
|
@ -30,5 +31,6 @@ export default combineReducers({
|
||||||
search,
|
search,
|
||||||
notifications,
|
notifications,
|
||||||
settings,
|
settings,
|
||||||
cards
|
cards,
|
||||||
|
reports
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import {
|
||||||
|
REPORT_INIT,
|
||||||
|
REPORT_SUBMIT_REQUEST,
|
||||||
|
REPORT_SUBMIT_SUCCESS,
|
||||||
|
REPORT_SUBMIT_FAIL,
|
||||||
|
REPORT_CANCEL,
|
||||||
|
REPORT_STATUS_TOGGLE
|
||||||
|
} from '../actions/reports';
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
|
const initialState = Immutable.Map({
|
||||||
|
new: Immutable.Map({
|
||||||
|
isSubmitting: false,
|
||||||
|
account_id: null,
|
||||||
|
status_ids: Immutable.Set(),
|
||||||
|
comment: ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
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 ? Immutable.Set([action.status.get('id')]) : Immutable.Set());
|
||||||
|
map.setIn(['new', 'comment'], '');
|
||||||
|
} else {
|
||||||
|
map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.get('id')));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
case REPORT_STATUS_TOGGLE:
|
||||||
|
return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => {
|
||||||
|
if (action.checked) {
|
||||||
|
return set.add(action.statusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return set.remove(action.statusId);
|
||||||
|
});
|
||||||
|
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'], Immutable.Set());
|
||||||
|
map.setIn(['new', 'comment'], '');
|
||||||
|
map.setIn(['new', 'isSubmitting'], false);
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -228,6 +228,14 @@ a.status__content__spoiler-link {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-check-box {
|
||||||
|
border-bottom: 1px solid lighten($color1, 8%);
|
||||||
|
|
||||||
|
.status__content {
|
||||||
|
background: lighten($color1, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status__prepend {
|
.status__prepend {
|
||||||
margin-left: 68px;
|
margin-left: 68px;
|
||||||
color: lighten($color1, 26%);
|
color: lighten($color1, 26%);
|
||||||
|
@ -1142,3 +1150,35 @@ button.active i.fa-retweet {
|
||||||
color: $color3;
|
color: $color3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report__target {
|
||||||
|
border-bottom: 1px solid lighten($color1, 4%);
|
||||||
|
color: $color2;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
color: $color5;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.report__textarea {
|
||||||
|
background: transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 2px solid $color3;
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
padding: 7px 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color5;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
outline: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
&:active, &:focus {
|
||||||
|
border-bottom-color: $color4;
|
||||||
|
background: rgba($color8, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -93,6 +93,7 @@ code {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
&:invalid {
|
&:invalid {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::ReportsController < ApiController
|
||||||
|
before_action -> { doorkeeper_authorize! :read }, except: [:create]
|
||||||
|
before_action -> { doorkeeper_authorize! :write }, only: [:create]
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
@reports = Report.where(account: current_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
status_ids = params[:status_ids].is_a?(Enumerable) ? params[:status_ids] : [params[:status_ids]]
|
||||||
|
|
||||||
|
@report = Report.create!(account: current_account,
|
||||||
|
target_account: Account.find(params[:account_id]),
|
||||||
|
status_ids: Status.find(status_ids).pluck(:id),
|
||||||
|
comment: params[:comment])
|
||||||
|
|
||||||
|
render :show
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Report < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :target_account, class_name: 'Account'
|
||||||
|
|
||||||
|
scope :unresolved, -> { where(action_taken: false) }
|
||||||
|
scope :resolved, -> { where(action_taken: true) }
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
collection @reports
|
||||||
|
extends 'api/v1/reports/show'
|
|
@ -0,0 +1,2 @@
|
||||||
|
object @report
|
||||||
|
attributes :id, :action_taken
|
|
@ -115,6 +115,7 @@ Rails.application.routes.draw do
|
||||||
resources :apps, only: [:create]
|
resources :apps, only: [:create]
|
||||||
resources :blocks, only: [:index]
|
resources :blocks, only: [:index]
|
||||||
resources :favourites, only: [:index]
|
resources :favourites, only: [:index]
|
||||||
|
resources :reports, only: [:index, :create]
|
||||||
|
|
||||||
resources :follow_requests, only: [:index] do
|
resources :follow_requests, only: [:index] do
|
||||||
member do
|
member do
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
class CreateReports < ActiveRecord::Migration[5.0]
|
||||||
|
def change
|
||||||
|
create_table :reports do |t|
|
||||||
|
t.integer :account_id, null: false
|
||||||
|
t.integer :target_account_id, null: false
|
||||||
|
t.integer :status_ids, array: true, null: false, default: []
|
||||||
|
t.text :comment, null: false, default: ''
|
||||||
|
t.boolean :action_taken, null: false, default: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
12
db/schema.rb
12
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20170209184350) do
|
ActiveRecord::Schema.define(version: 20170214110202) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -173,6 +173,16 @@ ActiveRecord::Schema.define(version: 20170209184350) do
|
||||||
t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree
|
t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "reports", force: :cascade do |t|
|
||||||
|
t.integer "account_id", null: false
|
||||||
|
t.integer "target_account_id", null: false
|
||||||
|
t.integer "status_ids", default: [], null: false, array: true
|
||||||
|
t.text "comment", default: "", null: false
|
||||||
|
t.boolean "action_taken", default: false, null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
create_table "settings", force: :cascade do |t|
|
create_table "settings", force: :cascade do |t|
|
||||||
t.string "var", null: false
|
t.string "var", null: false
|
||||||
t.text "value"
|
t.text "value"
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Fabricator(:report) do
|
||||||
|
comment "You nasty"
|
||||||
|
action_taken false
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Report, type: :model do
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in New Issue