Reports See merge request soapbox-pub/soapbox-fe!409merge-requests/410/merge
commit
ecd9e60a5d
@ -0,0 +1,149 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import Button from 'soapbox/components/button';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
|
||||
import Accordion from 'soapbox/features/ui/components/accordion';
|
||||
import ReportStatus from './report_status';
|
||||
import { closeReports, deactivateUsers, deleteUsers } from 'soapbox/actions/admin';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on {acct} was closed' },
|
||||
deactivateUser: { id: 'admin.reports.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' },
|
||||
deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate {acct}. Deactivating a user is a reversible action.' },
|
||||
deactivateUserConfirm: { id: 'confirmations.admin.deactivate_user.confirm', defaultMessage: 'Deactivate {acct}' },
|
||||
userDeactivated: { id: 'admin.reports.user_deactivated_message', defaultMessage: '{acct} was deactivated' },
|
||||
deleteUser: { id: 'admin.reports.actions.delete_user', defaultMessage: 'Delete {acct}' },
|
||||
deleteUserPrompt: { id: 'confirmations.admin.delete_user.message', defaultMessage: 'You are about to delete {acct}. THIS IS A DESTRUCTIVE ACTION THAT CANNOT BE UNDONE.' },
|
||||
deleteUserConfirm: { id: 'confirmations.admin.delete_user.confirm', defaultMessage: 'Delete {acct}' },
|
||||
userDeleted: { id: 'admin.reports.user_deleted_message', defaultMessage: '{acct} was deleted' },
|
||||
});
|
||||
|
||||
export default @connect()
|
||||
@injectIntl
|
||||
class Report extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
report: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
accordionExpanded: false,
|
||||
};
|
||||
|
||||
makeMenu = () => {
|
||||
const { intl, report } = this.props;
|
||||
|
||||
return [{
|
||||
text: intl.formatMessage(messages.deactivateUser, { acct: `@${report.getIn(['account', 'acct'])}` }),
|
||||
action: this.handleDeactivateUser,
|
||||
}, {
|
||||
text: intl.formatMessage(messages.deleteUser, { acct: `@${report.getIn(['account', 'acct'])}` }),
|
||||
action: this.handleDeleteUser,
|
||||
}];
|
||||
}
|
||||
|
||||
handleCloseReport = () => {
|
||||
const { intl, dispatch, report } = this.props;
|
||||
const nickname = report.getIn(['account', 'acct']);
|
||||
dispatch(closeReports([report.get('id')])).then(() => {
|
||||
const message = intl.formatMessage(messages.reportClosed, { acct: `@${nickname}` });
|
||||
dispatch(snackbar.success(message));
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
handleDeactivateUser = () => {
|
||||
const { intl, dispatch, report } = this.props;
|
||||
const nickname = report.getIn(['account', 'acct']);
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deactivateUserPrompt, { acct: `@${nickname}` }),
|
||||
confirm: intl.formatMessage(messages.deactivateUserConfirm, { acct: `@${nickname}` }),
|
||||
onConfirm: () => {
|
||||
dispatch(deactivateUsers([nickname])).then(() => {
|
||||
const message = intl.formatMessage(messages.userDeactivated, { acct: `@${nickname}` });
|
||||
dispatch(snackbar.success(message));
|
||||
}).catch(() => {});
|
||||
this.handleCloseReport();
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
handleDeleteUser = () => {
|
||||
const { intl, dispatch, report } = this.props;
|
||||
const nickname = report.getIn(['account', 'acct']);
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deleteUserPrompt, { acct: `@${nickname}` }),
|
||||
confirm: intl.formatMessage(messages.deleteUserConfirm, { acct: `@${nickname}` }),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteUsers([nickname])).then(() => {
|
||||
const message = intl.formatMessage(messages.userDeleted, { acct: `@${nickname}` });
|
||||
dispatch(snackbar.success(message));
|
||||
}).catch(() => {});
|
||||
this.handleCloseReport();
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
handleAccordionToggle = setting => {
|
||||
this.setState({ accordionExpanded: setting });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { report } = this.props;
|
||||
const { accordionExpanded } = this.state;
|
||||
const menu = this.makeMenu();
|
||||
const statuses = report.get('statuses');
|
||||
const statusCount = statuses.count();
|
||||
const acct = report.getIn(['account', 'acct']);
|
||||
const reporterAcct = report.getIn(['actor', 'acct']);
|
||||
|
||||
return (
|
||||
<div className='admin-report' key={report.get('id')}>
|
||||
<div className='admin-report__avatar'>
|
||||
<Link to={`/@${acct}`} title={acct}>
|
||||
<Avatar account={report.get('account')} size={32} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className='admin-report__content'>
|
||||
<h4 className='admin-report__title'>
|
||||
<FormattedMessage
|
||||
id='admin.reports.report_title'
|
||||
defaultMessage='Report on {acct}'
|
||||
values={{ acct: <Link to={`/@${acct}`} title={acct}>@{acct}</Link> }}
|
||||
/>
|
||||
</h4>
|
||||
<div className='admin-report__statuses'>
|
||||
{statusCount > 0 && (
|
||||
<Accordion
|
||||
headline={`Reported posts (${statusCount})`}
|
||||
expanded={accordionExpanded}
|
||||
onToggle={this.handleAccordionToggle}
|
||||
>
|
||||
{statuses.map(status => <ReportStatus report={report} status={status} key={status.get('id')} />)}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
<div className='admin-report__quote'>
|
||||
{report.get('content', '').length > 0 &&
|
||||
<blockquote className='md' dangerouslySetInnerHTML={{ __html: report.get('content') }} />
|
||||
}
|
||||
<span className='byline'>— <Link to={`/@${reporterAcct}`} title={reporterAcct}>@{reporterAcct}</Link></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='admin-report__actions'>
|
||||
<Button className='button-alternative' size={30} onClick={this.handleCloseReport}>
|
||||
<FormattedMessage id='admin.reports.actions.close' defaultMessage='Close' />
|
||||
</Button>
|
||||
<DropdownMenu items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import StatusContent from 'soapbox/components/status_content';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
|
||||
import { deleteStatus } from 'soapbox/actions/admin';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import noop from 'lodash/noop';
|
||||
import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components';
|
||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
|
||||
const messages = defineMessages({
|
||||
viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' },
|
||||
deleteStatus: { id: 'admin.reports.actions.delete_status', defaultMessage: 'Delete post' },
|
||||
deleteStatusPrompt: { id: 'confirmations.admin.delete_status.message', defaultMessage: 'You are about to delete a post by {acct}. This action cannot be undone.' },
|
||||
deleteStatusConfirm: { id: 'confirmations.admin.delete_status.confirm', defaultMessage: 'Delete post' },
|
||||
statusDeleted: { id: 'admin.reports.status_deleted_message', defaultMessage: 'Post by {acct} was deleted' },
|
||||
});
|
||||
|
||||
export default @connect()
|
||||
@injectIntl
|
||||
class ReportStatus extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
report: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
makeMenu = () => {
|
||||
const { intl, status } = this.props;
|
||||
const acct = status.getIn(['account', 'acct']);
|
||||
|
||||
return [{
|
||||
text: intl.formatMessage(messages.viewStatus, { acct: `@${acct}` }),
|
||||
to: `/@${acct}/posts/${status.get('id')}`,
|
||||
}, {
|
||||
text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }),
|
||||
action: this.handleDeleteStatus,
|
||||
}];
|
||||
}
|
||||
|
||||
getMedia = () => {
|
||||
const { status } = this.props;
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0]);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||
width={239}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const audio = status.getIn(['media_attachments', 0]);
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={audio.get('url')}
|
||||
alt={audio.get('description')}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenAudio={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.handleOpenMedia} />}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
handleOpenMedia = (media, index) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(openModal('MEDIA', { media, index }));
|
||||
}
|
||||
|
||||
handleDeleteStatus = () => {
|
||||
const { intl, dispatch, status } = this.props;
|
||||
const nickname = status.getIn(['account', 'acct']);
|
||||
const statusId = status.get('id');
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deleteStatusPrompt, { acct: `@${nickname}` }),
|
||||
confirm: intl.formatMessage(messages.deleteStatusConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteStatus(statusId)).then(() => {
|
||||
const message = intl.formatMessage(messages.statusDeleted, { acct: `@${nickname}` });
|
||||
dispatch(snackbar.success(message));
|
||||
}).catch(() => {});
|
||||
this.handleCloseReport();
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status } = this.props;
|
||||
const media = this.getMedia();
|
||||
const menu = this.makeMenu();
|
||||
|
||||
return (
|
||||
<div className='admin-report__status'>
|
||||
<div className='admin-report__status-content'>
|
||||
<StatusContent status={status} />
|
||||
{media}
|
||||
</div>
|
||||
<div className='admin-report__status-actions'>
|
||||
<DropdownMenu items={menu} icon='ellipsis-v' size={18} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from '../ui/components/column';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import { fetchReports } from 'soapbox/actions/admin';
|
||||
import Report from './components/report';
|
||||
import { makeGetReport } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.reports', defaultMessage: 'Reports' },
|
||||
emptyMessage: { id: 'admin.reports.empty_message', defaultMessage: 'There are no open reports. If a user gets reported, they will show up here.' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const getReport = makeGetReport();
|
||||
const ids = state.getIn(['admin', 'openReports']);
|
||||
|
||||
return {
|
||||
reports: ids.toList().map(id => getReport(state, id)),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Reports extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
reports: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: true,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchReports())
|
||||
.then(() => this.setState({ isLoading: false }))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, reports } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
const showLoading = isLoading && reports.count() === 0;
|
||||
|
||||
return (
|
||||
<Column icon='gavel' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<ScrollableList
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
scrollKey='admin-reports'
|
||||
emptyMessage={intl.formatMessage(messages.emptyMessage)}
|
||||
>
|
||||
{reports.map(report => <Report report={report} key={report.get('id')} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in new issue