Permalink
Browse files

Adding POST /api/v1/reports API, and a UI for submitting reports

  • Loading branch information...
Gargron committed Feb 14, 2017
1 parent 40a4053 commit 3b81baaaaf51ff1c70fb1f865eef07fdb33a5950
Showing with 480 additions and 10 deletions.
  1. +1 −1 app/assets/javascripts/components/actions/compose.jsx
  2. +64 −0 app/assets/javascripts/components/actions/reports.jsx
  3. +12 −2 app/assets/javascripts/components/components/status_action_bar.jsx
  4. +2 −0 app/assets/javascripts/components/containers/mastodon.jsx
  5. +5 −0 app/assets/javascripts/components/containers/status_container.jsx
  6. +9 −2 app/assets/javascripts/components/features/account/components/action_bar.jsx
  7. +8 −1 app/assets/javascripts/components/features/account_timeline/components/header.jsx
  8. +5 −0 app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
  9. +38 −0 app/assets/javascripts/components/features/report/components/status_check_box.jsx
  10. +19 −0 app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx
  11. +130 −0 app/assets/javascripts/components/features/report/index.jsx
  12. +9 −1 app/assets/javascripts/components/features/status/components/action_bar.jsx
  13. +6 −1 app/assets/javascripts/components/features/status/index.jsx
  14. +3 −1 app/assets/javascripts/components/reducers/index.jsx
  15. +57 −0 app/assets/javascripts/components/reducers/reports.jsx
  16. +40 −0 app/assets/stylesheets/components.scss
  17. +1 −0 app/assets/stylesheets/forms.scss
  18. +24 −0 app/controllers/api/v1/reports_controller.rb
  19. +9 −0 app/models/report.rb
  20. +2 −0 app/views/api/v1/reports/index.rabl
  21. +2 −0 app/views/api/v1/reports/show.rabl
  22. +1 −0 config/routes.rb
  23. +13 −0 db/migrate/20170214110202_create_reports.rb
  24. +11 −1 db/schema.rb
  25. +4 −0 spec/fabricators/report_fabricator.rb
  26. +5 −0 spec/models/report_spec.rb
@@ -1,4 +1,4 @@
import api from '../api'
import api from '../api';
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' },
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
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({
@@ -27,7 +28,10 @@ const StatusActionBar = React.createClass({
onReblog: React.PropTypes.func,
onDelete: 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],
@@ -60,6 +64,11 @@ const StatusActionBar = React.createClass({
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
},
handleReport () {
this.props.onReport(this.props.status);
this.context.router.push('/report');
},
render () {
const { status, me, intl } = this.props;
let menu = [];
@@ -71,6 +80,7 @@ const StatusActionBar = React.createClass({
} else {
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.report), action: this.handleReport });
}
return (
@@ -34,6 +34,7 @@ import FollowRequests from '../features/follow_requests';
import GenericNotFound from '../features/generic_not_found';
import FavouritedStatuses from '../features/favourited_statuses';
import Blocks from '../features/blocks';
import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de';
@@ -131,6 +132,7 @@ const Mastodon = React.createClass({
<Route path='follow_requests' component={FollowRequests} />
<Route path='blocks' component={Blocks} />
<Route path='report' component={Report} />
<Route path='*' component={GenericNotFound} />
</Route>
@@ -13,6 +13,7 @@ import {
} from '../actions/interactions';
import { blockAccount } from '../actions/accounts';
import { deleteStatus } from '../actions/statuses';
import { initReport } from '../actions/reports';
import { openMedia } from '../actions/modal';
import { createSelector } from 'reselect'
import { isMobile } from '../is_mobile'
@@ -97,6 +98,10 @@ const mapDispatchToProps = (dispatch) => ({
onBlock (account) {
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' },
block: { id: 'account.block', defaultMessage: 'Block' },
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 = {
@@ -32,7 +33,9 @@ const ActionBar = React.createClass({
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func,
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],
@@ -54,6 +57,10 @@ const ActionBar = React.createClass({
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 (
<div className='account__action-bar'>
<div style={outerDropdownStyle}>
@@ -13,7 +13,8 @@ const Header = React.createClass({
me: React.PropTypes.number.isRequired,
onFollow: 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],
@@ -30,6 +31,11 @@ const Header = React.createClass({
this.props.onMention(this.props.account, this.context.router);
},
handleReport () {
this.props.onReport(this.props.account);
this.context.router.push('/report');
},
render () {
const { account, me } = this.props;
@@ -50,6 +56,7 @@ const Header = React.createClass({
me={me}
onBlock={this.handleBlock}
onMention={this.handleMention}
onReport={this.handleReport}
/>
</div>
);
@@ -8,6 +8,7 @@ import {
unblockAccount
} from '../../../actions/accounts';
import { mentionCompose } from '../../../actions/compose';
import { initReport } from '../../../actions/reports';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
@@ -39,6 +40,10 @@ const mapDispatchToProps = dispatch => ({
onMention (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);
Oops, something went wrong.

0 comments on commit 3b81baa

Please sign in to comment.