@@ -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 |
@@ -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 |