Adding POST /api/v1/reports API, and a UI for submitting reports
This commit is contained in:
		| @@ -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 | ||||
		Reference in New Issue
	
	Block a user