forked from mirrors/catstodon
Adding POST /api/v1/reports API, and a UI for submitting reports
This commit is contained in:
parent
40a4053732
commit
3b81baaaaf
26 changed files with 480 additions and 10 deletions
|
@ -1,4 +1,4 @@
|
|||
import api from '../api'
|
||||
import api from '../api';
|
||||
|
||||
import { updateTimeline } from './timelines';
|
||||
|
||||
|
|
64
app/assets/javascripts/components/actions/reports.jsx
Normal file
64
app/assets/javascripts/components/actions/reports.jsx
Normal file
|
@ -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);
|
130
app/assets/javascripts/components/features/report/index.jsx
Normal file
130
app/assets/javascripts/components/features/report/index.jsx
Normal file
|
@ -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' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
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({
|
||||
|
@ -25,6 +26,7 @@ const ActionBar = React.createClass({
|
|||
onFavourite: React.PropTypes.func.isRequired,
|
||||
onDelete: React.PropTypes.func.isRequired,
|
||||
onMention: React.PropTypes.func.isRequired,
|
||||
onReport: React.PropTypes.func,
|
||||
me: React.PropTypes.number.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);
|
||||
},
|
||||
|
||||
handleReport () {
|
||||
this.props.onReport(this.props.status);
|
||||
this.context.router.push('/report');
|
||||
},
|
||||
|
||||
render () {
|
||||
const { status, me, intl } = this.props;
|
||||
|
||||
|
@ -60,6 +67,7 @@ const ActionBar = React.createClass({
|
|||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
|
||||
menu.push({ text: intl.formatMessage(messages.report), action: this.handleReport });
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mentionCompose
|
||||
} from '../../actions/compose';
|
||||
import { deleteStatus } from '../../actions/statuses';
|
||||
import { initReport } from '../../actions/reports';
|
||||
import {
|
||||
makeGetStatus,
|
||||
getStatusAncestors,
|
||||
|
@ -88,6 +89,10 @@ const Status = React.createClass({
|
|||
this.props.dispatch(openMedia(media, index));
|
||||
},
|
||||
|
||||
handleReport (status) {
|
||||
this.props.dispatch(initReport(status.get('account'), status));
|
||||
},
|
||||
|
||||
renderChildren (list) {
|
||||
return list.map(id => <StatusContainer key={id} id={id} />);
|
||||
},
|
||||
|
@ -123,7 +128,7 @@ const Status = React.createClass({
|
|||
{ancestors}
|
||||
|
||||
<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}
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,7 @@ import notifications from './notifications';
|
|||
import settings from './settings';
|
||||
import status_lists from './status_lists';
|
||||
import cards from './cards';
|
||||
import reports from './reports';
|
||||
|
||||
export default combineReducers({
|
||||
timelines,
|
||||
|
@ -30,5 +31,6 @@ export default combineReducers({
|
|||
search,
|
||||
notifications,
|
||||
settings,
|
||||
cards
|
||||
cards,
|
||||
reports
|
||||
});
|
||||
|
|
57
app/assets/javascripts/components/reducers/reports.jsx
Normal file
57
app/assets/javascripts/components/reducers/reports.jsx
Normal file
|
@ -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 {
|
||||
margin-left: 68px;
|
||||
color: lighten($color1, 26%);
|
||||
|
@ -1142,3 +1150,35 @@ button.active i.fa-retweet {
|
|||
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%;
|
||||
outline: 0;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
|
||||
&:invalid {
|
||||
box-shadow: none;
|
||||
|
|
24
app/controllers/api/v1/reports_controller.rb
Normal file
24
app/controllers/api/v1/reports_controller.rb
Normal file
|
@ -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
|
9
app/models/report.rb
Normal file
9
app/models/report.rb
Normal file
|
@ -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
|
2
app/views/api/v1/reports/index.rabl
Normal file
2
app/views/api/v1/reports/index.rabl
Normal file
|
@ -0,0 +1,2 @@
|
|||
collection @reports
|
||||
extends 'api/v1/reports/show'
|
2
app/views/api/v1/reports/show.rabl
Normal file
2
app/views/api/v1/reports/show.rabl
Normal file
|
@ -0,0 +1,2 @@
|
|||
object @report
|
||||
attributes :id, :action_taken
|
|
@ -115,6 +115,7 @@ Rails.application.routes.draw do
|
|||
resources :apps, only: [:create]
|
||||
resources :blocks, only: [:index]
|
||||
resources :favourites, only: [:index]
|
||||
resources :reports, only: [:index, :create]
|
||||
|
||||
resources :follow_requests, only: [:index] do
|
||||
member do
|
||||
|
|
13
db/migrate/20170214110202_create_reports.rb
Normal file
13
db/migrate/20170214110202_create_reports.rb
Normal file
|
@ -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.
|
||||
|
||||
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
|
||||
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
|
||||
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|
|
||||
t.string "var", null: false
|
||||
t.text "value"
|
||||
|
|
4
spec/fabricators/report_fabricator.rb
Normal file
4
spec/fabricators/report_fabricator.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
Fabricator(:report) do
|
||||
comment "You nasty"
|
||||
action_taken false
|
||||
end
|
5
spec/models/report_spec.rb
Normal file
5
spec/models/report_spec.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Report, type: :model do
|
||||
|
||||
end
|
Loading…
Reference in a new issue