From 51faa660ca2b52c3296d81b33f261a8ba45cc1a0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 14:29:31 -0600 Subject: [PATCH 01/22] Admin: refactor Reports reducer --- app/soapbox/actions/admin.js | 4 ++-- app/soapbox/components/helmet.js | 2 +- .../features/admin/components/admin_nav.js | 2 +- .../components/registration_mode_picker.js | 1 - .../features/ui/components/tabs_bar.js | 2 +- app/soapbox/reducers/__tests__/admin-test.js | 9 ++++---- app/soapbox/reducers/admin.js | 23 +++++++++++-------- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index ecf70923a..a6b38f962 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -55,8 +55,8 @@ export function fetchReports(params) { dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); return api(getState) .get('/api/pleroma/admin/reports', { params }) - .then(({ data }) => { - dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, data, params }); + .then(({ data: { reports } }) => { + dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params }); }).catch(error => { dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); }); diff --git a/app/soapbox/components/helmet.js b/app/soapbox/components/helmet.js index 080f234c1..d89b36aae 100644 --- a/app/soapbox/components/helmet.js +++ b/app/soapbox/components/helmet.js @@ -6,7 +6,7 @@ import { Helmet } from'react-helmet'; const getNotifTotals = state => { const notifications = state.getIn(['notifications', 'unread'], 0); const chats = state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0); - const reports = state.getIn(['admin', 'open_report_count'], 0); + const reports = state.getIn(['admin', 'openReports']).count(); const approvals = state.getIn(['admin', 'awaitingApproval']).count(); return notifications + chats + reports + approvals; }; diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 1d0a9a47f..6f5e69c95 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -10,7 +10,7 @@ import { FormattedMessage } from 'react-intl'; const mapStateToProps = (state, props) => ({ instance: state.get('instance'), approvalCount: state.getIn(['admin', 'awaitingApproval']).count(), - reportsCount: state.getIn(['admin', 'open_report_count']), + reportsCount: state.getIn(['admin', 'openReports']).count(), }); export default @connect(mapStateToProps) diff --git a/app/soapbox/features/admin/components/registration_mode_picker.js b/app/soapbox/features/admin/components/registration_mode_picker.js index dd007dba4..1e2f153ae 100644 --- a/app/soapbox/features/admin/components/registration_mode_picker.js +++ b/app/soapbox/features/admin/components/registration_mode_picker.js @@ -17,7 +17,6 @@ const messages = defineMessages({ const mapStateToProps = (state, props) => ({ mode: modeFromInstance(state.get('instance')), - openReportCount: state.getIn(['admin', 'open_report_count']), }); const generateConfig = mode => { diff --git a/app/soapbox/features/ui/components/tabs_bar.js b/app/soapbox/features/ui/components/tabs_bar.js index 013eef313..fa4a685f4 100644 --- a/app/soapbox/features/ui/components/tabs_bar.js +++ b/app/soapbox/features/ui/components/tabs_bar.js @@ -154,7 +154,7 @@ class TabsBar extends React.PureComponent { const mapStateToProps = state => { const me = state.get('me'); - const reportsCount = state.getIn(['admin', 'open_report_count']); + const reportsCount = state.getIn(['admin', 'openReports']).count(); const approvalCount = state.getIn(['admin', 'awaitingApproval']).count(); return { account: state.getIn(['accounts', me]), diff --git a/app/soapbox/reducers/__tests__/admin-test.js b/app/soapbox/reducers/__tests__/admin-test.js index 9f983af0a..4f9ce0aa9 100644 --- a/app/soapbox/reducers/__tests__/admin-test.js +++ b/app/soapbox/reducers/__tests__/admin-test.js @@ -3,18 +3,17 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, - fromJS, } from 'immutable'; describe('admin reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(fromJS({ - reports: [], - open_report_count: 0, + expect(reducer(undefined, {})).toEqual({ + reports: ImmutableMap(), + openReports: ImmutableOrderedSet(), users: ImmutableMap(), awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), needsReboot: false, - })); + }); }); }); diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index eaacf7323..bdcdcd9ec 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -15,9 +15,9 @@ import { } from 'immutable'; const initialState = ImmutableMap({ - reports: ImmutableList(), + reports: ImmutableMap(), + openReports: ImmutableOrderedSet(), users: ImmutableMap(), - open_report_count: 0, awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), needsReboot: false, @@ -52,18 +52,23 @@ function approveUsers(state, users) { }); } +function importReports(state, reports) { + return state.withMutations(state => { + reports.forEach(report => { + if (report.state === 'open') { + state.update('openReports', orderedSet => orderedSet.add(report.id)); + } + state.setIn(['reports', report.id], fromJS(report)); + }); + }); +} + export default function admin(state = initialState, action) { switch(action.type) { case ADMIN_CONFIG_FETCH_SUCCESS: return state.set('configs', fromJS(action.configs)); case ADMIN_REPORTS_FETCH_SUCCESS: - if (action.params && action.params.state === 'open') { - return state - .set('reports', fromJS(action.data.reports)) - .set('open_report_count', action.data.total); - } else { - return state.set('reports', fromJS(action.data.reports)); - } + return importReports(state, action.reports); case ADMIN_USERS_FETCH_SUCCESS: return importUsers(state, action.data.users); case ADMIN_USERS_DELETE_REQUEST: From 8b936036d051175fedcf6992f54f9948cbc2ea15 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 14:37:35 -0600 Subject: [PATCH 02/22] Admin: improve awaiting-approval showLoading logic --- app/soapbox/features/admin/awaiting_approval.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js index 94c7284c9..5d74435ae 100644 --- a/app/soapbox/features/admin/awaiting_approval.js +++ b/app/soapbox/features/admin/awaiting_approval.js @@ -68,10 +68,11 @@ class AwaitingApproval extends ImmutablePureComponent { render() { const { intl, users } = this.props; const { isLoading } = this.state; + const showLoading = isLoading && users.count() === 0; return ( - + {users.map((user, i) => (
From f7d11ff36e18a692439117037359246c17de4466 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 14:41:43 -0600 Subject: [PATCH 03/22] Admin: reports boilerplate --- .../features/admin/components/admin_nav.js | 4 +- app/soapbox/features/admin/reports.js | 61 +++++++++++++++++++ app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 ++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 app/soapbox/features/admin/reports.js diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 6f5e69c95..1c0a17ea9 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -33,10 +33,10 @@ class AdminNav extends React.PureComponent { - + - + {((instance.get('registrations') && instance.get('approval_required')) || approvalCount > 0) && ( diff --git a/app/soapbox/features/admin/reports.js b/app/soapbox/features/admin/reports.js new file mode 100644 index 000000000..85129c9ce --- /dev/null +++ b/app/soapbox/features/admin/reports.js @@ -0,0 +1,61 @@ +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'; + +const messages = defineMessages({ + heading: { id: 'column.admin.reports', defaultMessage: 'Reports' }, + emptyMessage: { id: 'admin.reports.empty_message', defaultMessage: 'There are no open reports. When a user reports a post, it will show up here.' }, +}); + +const mapStateToProps = state => { + const ids = state.getIn(['admin', 'openReports']); + return { + reports: ids.toList().map(id => state.getIn(['admin', 'reports', 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 ( + + + {reports.map(report => ( +
+ {report.get('id')} +
+ ))} +
+
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 806f56bfe..59c36c831 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -89,6 +89,7 @@ import { ServerInfo, Dashboard, AwaitingApproval, + Reports, } from './util/async-components'; // Dummy import, to make sure that ends up in the application bundle. @@ -280,6 +281,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 6e313b4d8..1e9cc7550 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -225,3 +225,7 @@ export function Dashboard() { export function AwaitingApproval() { return import(/* webpackChunkName: "features/admin/awaiting_approval" */'../../admin/awaiting_approval'); } + +export function Reports() { + return import(/* webpackChunkName: "features/admin/reports" */'../../admin/reports'); +} From 0837bd0495afe319bff1e9f633ded86572621419 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 15:05:39 -0600 Subject: [PATCH 04/22] Admin Reports: display basic information --- app/soapbox/features/admin/reports.js | 3 ++- app/styles/components/admin.scss | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/reports.js b/app/soapbox/features/admin/reports.js index 85129c9ce..a277eae63 100644 --- a/app/soapbox/features/admin/reports.js +++ b/app/soapbox/features/admin/reports.js @@ -50,7 +50,8 @@ class Reports extends ImmutablePureComponent { {reports.map(report => (
- {report.get('id')} +
Report on @{report.getIn(['account', 'acct'])}
+
{report.get('content')} — @{report.getIn(['actor', 'acct'])}
))}
diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index b544d9f70..4c25eab4d 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -119,3 +119,7 @@ } } } + +.admin-report { + padding: 20px; +} From a16e709ff4d5a93946732b32aaea98edfb3fcc4e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 17:20:46 -0600 Subject: [PATCH 05/22] Reports: break out separate Report component, basic styling --- .../intersection_observer_article.js | 1 + .../features/admin/awaiting_approval.js | 2 +- .../features/admin/components/report.js | 36 +++++++++++++++++++ app/soapbox/features/admin/reports.js | 15 ++++---- app/styles/components/admin.scss | 32 +++++++++++++---- 5 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 app/soapbox/features/admin/components/report.js diff --git a/app/soapbox/components/intersection_observer_article.js b/app/soapbox/components/intersection_observer_article.js index d21ea2565..12ebe20a2 100644 --- a/app/soapbox/components/intersection_observer_article.js +++ b/app/soapbox/components/intersection_observer_article.js @@ -24,6 +24,7 @@ export default class IntersectionObserverArticle extends React.Component { state = { isHidden: false, // set to true in requestIdleCallback to trigger un-render + isIntersecting: true, } shouldComponentUpdate(nextProps, nextState) { diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js index 5d74435ae..7706eb4a8 100644 --- a/app/soapbox/features/admin/awaiting_approval.js +++ b/app/soapbox/features/admin/awaiting_approval.js @@ -77,7 +77,7 @@ class AwaitingApproval extends ImmutablePureComponent {
@{user.get('nickname')}
-
{user.get('registration_reason')}
+
{user.get('registration_reason')}
diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js new file mode 100644 index 000000000..0365e3cf2 --- /dev/null +++ b/app/soapbox/features/admin/components/report.js @@ -0,0 +1,36 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; +import Avatar from 'soapbox/components/avatar'; + +export default class Report extends ImmutablePureComponent { + + static propTypes = { + report: ImmutablePropTypes.map.isRequired, + }; + + render() { + const { report } = this.props; + + return ( +
+ +
+

+ +

+
+
{report.get('content')}
+ — @{report.getIn(['actor', 'acct'])} +
+
+
+ ); + } + +} diff --git a/app/soapbox/features/admin/reports.js b/app/soapbox/features/admin/reports.js index a277eae63..1acc6def1 100644 --- a/app/soapbox/features/admin/reports.js +++ b/app/soapbox/features/admin/reports.js @@ -7,6 +7,7 @@ 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'; const messages = defineMessages({ heading: { id: 'column.admin.reports', defaultMessage: 'Reports' }, @@ -47,13 +48,13 @@ class Reports extends ImmutablePureComponent { return ( - - {reports.map(report => ( -
-
Report on @{report.getIn(['account', 'acct'])}
-
{report.get('content')} — @{report.getIn(['actor', 'acct'])}
-
- ))} + + {reports.map(report => )}
); diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index 4c25eab4d..4942144b6 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -77,12 +77,6 @@ font-weight: bold; } - &__reason { - padding: 5px 0 5px 15px; - border-left: 3px solid hsla(var(--primary-text-color_hsl), 0.4); - color: var(--primary-text-color--faint); - } - &__actions { margin-left: auto; display: flex; @@ -118,8 +112,32 @@ } } } + + blockquote.md { + padding: 5px 0 5px 15px; + border-left: 3px solid hsla(var(--primary-text-color_hsl), 0.4); + color: var(--primary-text-color--faint); + } } .admin-report { - padding: 20px; + padding: 15px; + display: flex; + border-bottom: 1px solid var(--brand-color--faint); + + &__content { + padding-left: 16px; + } + + &__title { + font-weight: bold; + } + + &__quote { + font-size: 14px; + + .byline { + font-size: 12px; + } + } } From 95c4ba4234b97482d13c9258348f44e5fb62b63e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 17:54:08 -0600 Subject: [PATCH 06/22] Reports: allow deactivating a user --- app/soapbox/actions/admin.js | 17 ++++++++ .../features/admin/components/report.js | 41 +++++++++++++++++-- app/styles/components/admin.scss | 6 ++- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index a6b38f962..3592b20bc 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -24,6 +24,10 @@ export const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST'; export const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS'; export const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL'; +export const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST'; +export const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS'; +export const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL'; + export function fetchConfig() { return (dispatch, getState) => { dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); @@ -76,6 +80,19 @@ export function fetchUsers(params) { }; } +export function deactivateUsers(nicknames) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, nicknames }); + return api(getState) + .patch('/api/pleroma/admin/users/deactivate', { nicknames }) + .then(({ data: { users } }) => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, nicknames }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, nicknames }); + }); + }; +} + export function deleteUsers(nicknames) { return (dispatch, getState) => { dispatch({ type: ADMIN_USERS_DELETE_REQUEST, nicknames }); diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index 0365e3cf2..d0161f441 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -1,21 +1,53 @@ import React from 'react'; +import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; +import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import Avatar from 'soapbox/components/avatar'; +import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; +import { deactivateUsers } from 'soapbox/actions/admin'; +import snackbar from 'soapbox/actions/snackbar'; -export default class Report extends ImmutablePureComponent { +const messages = defineMessages({ + deactivateUser: { id: 'admin.reports.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' }, + deactivated: { id: 'admin.reports.deactivated_message', defaultMessage: '{acct} was deactivated' }, +}); + +export default @connect() +@injectIntl +class Report extends ImmutablePureComponent { static propTypes = { report: ImmutablePropTypes.map.isRequired, }; + makeMenu = () => { + const { intl, report } = this.props; + + return [{ + text: intl.formatMessage(messages.deactivateUser, { acct: `@${report.getIn(['account', 'acct'])}` }), + action: this.handleDeactivateUser, + }]; + } + + handleDeactivateUser = () => { + const { intl, dispatch, report } = this.props; + const nickname = report.getIn(['account', 'acct']); + dispatch(deactivateUsers([nickname])).then(() => { + const message = intl.formatMessage(messages.deactivated, { acct: nickname }); + dispatch(snackbar.success(message)); + }).catch(() => {}); + } + render() { const { report } = this.props; + const menu = this.makeMenu(); return (
- +
+ +

— @{report.getIn(['actor', 'acct'])}

+
+ +
); } diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index 4942144b6..a46265c67 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -126,7 +126,7 @@ border-bottom: 1px solid var(--brand-color--faint); &__content { - padding-left: 16px; + padding: 0 16px; } &__title { @@ -140,4 +140,8 @@ font-size: 12px; } } + + &__actions { + margin-left: auto; + } } From 35a148d9c957c4727edbc931e7a2ca487b4a9ce5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 18:24:52 -0600 Subject: [PATCH 07/22] Reports: allow closing a report --- app/soapbox/actions/admin.js | 21 +++++++++++++++++++ .../features/admin/components/report.js | 20 +++++++++++++++--- app/soapbox/reducers/admin.js | 21 +++++++++++++++++++ app/styles/components/admin.scss | 11 ++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 3592b20bc..84ec80362 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -12,6 +12,10 @@ export const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST'; export const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS'; export const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL'; +export const ADMIN_REPORTS_PATCH_REQUEST = 'ADMIN_REPORTS_PATCH_REQUEST'; +export const ADMIN_REPORTS_PATCH_SUCCESS = 'ADMIN_REPORTS_PATCH_SUCCESS'; +export const ADMIN_REPORTS_PATCH_FAIL = 'ADMIN_REPORTS_PATCH_FAIL'; + export const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST'; export const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS'; export const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL'; @@ -67,6 +71,23 @@ export function fetchReports(params) { }; } +function patchReports(ids, state) { + const reports = ids.map(id => ({ id, state })); + return (dispatch, getState) => { + dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports }); + return api(getState) + .patch('/api/pleroma/admin/reports', { reports }) + .then(() => { + dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports }); + }).catch(error => { + dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports }); + }); + }; +} +export function closeReports(ids) { + return patchReports(ids, 'closed'); +} + export function fetchUsers(params) { return (dispatch, getState) => { dispatch({ type: ADMIN_USERS_FETCH_REQUEST, params }); diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index d0161f441..6b6fa2098 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -4,13 +4,15 @@ 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 { deactivateUsers } from 'soapbox/actions/admin'; +import { deactivateUsers, closeReports } from 'soapbox/actions/admin'; import snackbar from 'soapbox/actions/snackbar'; const messages = defineMessages({ deactivateUser: { id: 'admin.reports.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' }, - deactivated: { id: 'admin.reports.deactivated_message', defaultMessage: '{acct} was deactivated' }, + userDeactivated: { id: 'admin.reports.user_deactivated_message', defaultMessage: '{acct} was deactivated' }, + reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on {acct} was closed' }, }); export default @connect() @@ -30,11 +32,20 @@ class Report extends ImmutablePureComponent { }]; } + 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(deactivateUsers([nickname])).then(() => { - const message = intl.formatMessage(messages.deactivated, { acct: nickname }); + const message = intl.formatMessage(messages.userDeactivated, { acct: `@${nickname}` }); dispatch(snackbar.success(message)); }).catch(() => {}); } @@ -62,6 +73,9 @@ class Report extends ImmutablePureComponent {
+
diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index bdcdcd9ec..36e5f77fe 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -1,6 +1,8 @@ import { ADMIN_CONFIG_FETCH_SUCCESS, ADMIN_REPORTS_FETCH_SUCCESS, + ADMIN_REPORTS_PATCH_REQUEST, + ADMIN_REPORTS_PATCH_SUCCESS, ADMIN_USERS_FETCH_SUCCESS, ADMIN_USERS_DELETE_REQUEST, ADMIN_USERS_DELETE_SUCCESS, @@ -63,12 +65,31 @@ function importReports(state, reports) { }); } +function handleReportDiffs(state, reports) { + // Note: the reports here aren't full report objects + // hence the need for a new function. + return state.withMutations(state => { + reports.forEach(report => { + switch(report.state) { + case 'open': + state.update('openReports', orderedSet => orderedSet.add(report.id)); + break; + default: + state.update('openReports', orderedSet => orderedSet.delete(report.id)); + } + }); + }); +} + export default function admin(state = initialState, action) { switch(action.type) { case ADMIN_CONFIG_FETCH_SUCCESS: return state.set('configs', fromJS(action.configs)); case ADMIN_REPORTS_FETCH_SUCCESS: return importReports(state, action.reports); + case ADMIN_REPORTS_PATCH_REQUEST: + case ADMIN_REPORTS_PATCH_SUCCESS: + return handleReportDiffs(state, action.reports); case ADMIN_USERS_FETCH_SUCCESS: return importUsers(state, action.data.users); case ADMIN_USERS_DELETE_REQUEST: diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index a46265c67..7899634c4 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -143,5 +143,16 @@ &__actions { margin-left: auto; + display: flex; + + .icon-button { + padding-left: 10px; + + > div { + display: flex; + align-items: center; + justify-content: center; + } + } } } From 2c58396b004ed646a3c57849c637633b17680399 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 18:31:50 -0600 Subject: [PATCH 08/22] Reports: tweak empty message text --- app/soapbox/features/admin/reports.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/reports.js b/app/soapbox/features/admin/reports.js index 1acc6def1..1621fbd73 100644 --- a/app/soapbox/features/admin/reports.js +++ b/app/soapbox/features/admin/reports.js @@ -11,7 +11,7 @@ import Report from './components/report'; const messages = defineMessages({ heading: { id: 'column.admin.reports', defaultMessage: 'Reports' }, - emptyMessage: { id: 'admin.reports.empty_message', defaultMessage: 'There are no open reports. When a user reports a post, it will show up here.' }, + 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 => { From dccc384f0d27bf3beab82f937a0e2604e7140b66 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 18:33:45 -0600 Subject: [PATCH 09/22] Reports: fix admin reducer test --- app/soapbox/reducers/__tests__/admin-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/reducers/__tests__/admin-test.js b/app/soapbox/reducers/__tests__/admin-test.js index 4f9ce0aa9..588abe7aa 100644 --- a/app/soapbox/reducers/__tests__/admin-test.js +++ b/app/soapbox/reducers/__tests__/admin-test.js @@ -7,13 +7,13 @@ import { describe('admin reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual({ + expect(reducer(undefined, {})).toEqual(ImmutableMap({ reports: ImmutableMap(), openReports: ImmutableOrderedSet(), users: ImmutableMap(), awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), needsReboot: false, - }); + })); }); }); From 48bca704ccf9971154433cec47bccf5353553167 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 18:44:45 -0600 Subject: [PATCH 10/22] Reports: close report when deactivating user --- app/soapbox/features/admin/components/report.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index 6b6fa2098..a2063c0ce 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -48,6 +48,7 @@ class Report extends ImmutablePureComponent { const message = intl.formatMessage(messages.userDeactivated, { acct: `@${nickname}` }); dispatch(snackbar.success(message)); }).catch(() => {}); + this.handleCloseReport(); } render() { From 043bb1a00ac9439d595740dec8e73db9cd1d0dbd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 18:51:51 -0600 Subject: [PATCH 11/22] Reports: add modal prompt when deactivating user --- .../features/admin/components/report.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index a2063c0ce..bff664c63 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -8,11 +8,14 @@ import Button from 'soapbox/components/button'; import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; import { deactivateUsers, closeReports } from 'soapbox/actions/admin'; import snackbar from 'soapbox/actions/snackbar'; +import { openModal } from 'soapbox/actions/modal'; const messages = defineMessages({ deactivateUser: { id: 'admin.reports.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' }, userDeactivated: { id: 'admin.reports.user_deactivated_message', defaultMessage: '{acct} was deactivated' }, reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on {acct} was closed' }, + 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}' }, }); export default @connect() @@ -44,11 +47,17 @@ class Report extends ImmutablePureComponent { handleDeactivateUser = () => { const { intl, dispatch, report } = this.props; const nickname = report.getIn(['account', 'acct']); - dispatch(deactivateUsers([nickname])).then(() => { - const message = intl.formatMessage(messages.userDeactivated, { acct: `@${nickname}` }); - dispatch(snackbar.success(message)); - }).catch(() => {}); - this.handleCloseReport(); + 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(); + }, + })); } render() { From 90414939a5642cb4f9687ad04afdd23c5665053c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 18:59:10 -0600 Subject: [PATCH 12/22] Reports: allow deleting a user --- .../features/admin/components/report.js | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index bff664c63..36347e0d0 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -6,16 +6,20 @@ 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 { deactivateUsers, closeReports } from 'soapbox/actions/admin'; +import { closeReports, deactivateUsers, deleteUsers } from 'soapbox/actions/admin'; import snackbar from 'soapbox/actions/snackbar'; import { openModal } from 'soapbox/actions/modal'; const messages = defineMessages({ - deactivateUser: { id: 'admin.reports.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' }, - userDeactivated: { id: 'admin.reports.user_deactivated_message', defaultMessage: '{acct} was deactivated' }, 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() @@ -32,6 +36,9 @@ class Report extends ImmutablePureComponent { 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, }]; } @@ -60,6 +67,22 @@ class Report extends ImmutablePureComponent { })); } + 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(); + }, + })); + } + render() { const { report } = this.props; const menu = this.makeMenu(); From a8893907d4cf8222ae0befacc08bb07ea3b1565a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 20:11:08 -0600 Subject: [PATCH 13/22] Reports: import statuses, add getReport selector --- app/soapbox/actions/admin.js | 2 ++ app/soapbox/features/admin/reports.js | 5 ++++- app/soapbox/reducers/admin.js | 1 + app/soapbox/selectors/index.js | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 84ec80362..94d8b2395 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -1,4 +1,5 @@ import api from '../api'; +import { importFetchedStatuses } from 'soapbox/actions/importer'; export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST'; export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS'; @@ -64,6 +65,7 @@ export function fetchReports(params) { return api(getState) .get('/api/pleroma/admin/reports', { params }) .then(({ data: { reports } }) => { + reports.forEach(report => dispatch(importFetchedStatuses(report.statuses))); dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params }); }).catch(error => { dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); diff --git a/app/soapbox/features/admin/reports.js b/app/soapbox/features/admin/reports.js index 1621fbd73..55c638b64 100644 --- a/app/soapbox/features/admin/reports.js +++ b/app/soapbox/features/admin/reports.js @@ -8,6 +8,7 @@ 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' }, @@ -15,9 +16,11 @@ const messages = defineMessages({ }); const mapStateToProps = state => { + const getReport = makeGetReport(); const ids = state.getIn(['admin', 'openReports']); + return { - reports: ids.toList().map(id => state.getIn(['admin', 'reports', id])), + reports: ids.map(id => getReport(state, id)), }; }; diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index 36e5f77fe..9f6000ec8 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -57,6 +57,7 @@ function approveUsers(state, users) { function importReports(state, reports) { return state.withMutations(state => { reports.forEach(report => { + report.statuses = report.statuses.map(status => status.id); if (report.state === 'open') { state.update('openReports', orderedSet => orderedSet.add(report.id)); } diff --git a/app/soapbox/selectors/index.js b/app/soapbox/selectors/index.js index 89d20b209..d36839d6e 100644 --- a/app/soapbox/selectors/index.js +++ b/app/soapbox/selectors/index.js @@ -175,3 +175,18 @@ export const makeGetChat = () => { }, ); }; + +export const makeGetReport = () => { + return createSelector( + [ + (state, id) => state.getIn(['admin', 'reports', id]), + (state, id) => state.getIn(['admin', 'reports', id, 'statuses']).map( + statusId => state.getIn(['statuses', statusId])), + ], + + (report, statuses) => { + if (!report) return null; + return report.set('statuses', statuses); + }, + ); +}; From a1db330b145d15ddda0990711776c309d0ab55cb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 20:18:57 -0600 Subject: [PATCH 14/22] Accordion: use children instead of content --- app/soapbox/features/public_timeline/index.js | 37 +++++++++---------- app/soapbox/features/soapbox_config/index.js | 21 +++++------ .../features/ui/components/accordion.js | 6 +-- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/app/soapbox/features/public_timeline/index.js b/app/soapbox/features/public_timeline/index.js index 9173274fb..0708ca1c1 100644 --- a/app/soapbox/features/public_timeline/index.js +++ b/app/soapbox/features/public_timeline/index.js @@ -92,27 +92,26 @@ class CommunityTimeline extends React.PureComponent {
} - content={( - - - - ), - }} - /> - )} expanded={explanationBoxExpanded} onToggle={this.toggleExplanationBox} - /> + > + + + + ), + }} + /> +
- - - )} expanded={this.state.jsonEditorExpanded} onToggle={this.toggleJSONEditor} - /> + > +
+ +
+
- {content} + {children}
); From 2fd3c9441c8c06b4546e046ec2549de54b26cd46 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 20:47:15 -0600 Subject: [PATCH 15/22] Reports: display reported posts --- .../features/admin/components/report.js | 22 ++++++++++++++++++- app/styles/components/accordion.scss | 1 + app/styles/components/admin.scss | 21 ++++++++++++++++++ app/styles/components/buttons.scss | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index 36347e0d0..2f6111795 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -5,7 +5,9 @@ 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 StatusContent from 'soapbox/components/status_content'; import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; +import Accordion from 'soapbox/features/ui/components/accordion'; import { closeReports, deactivateUsers, deleteUsers } from 'soapbox/actions/admin'; import snackbar from 'soapbox/actions/snackbar'; import { openModal } from 'soapbox/actions/modal'; @@ -30,6 +32,10 @@ class Report extends ImmutablePureComponent { report: ImmutablePropTypes.map.isRequired, }; + state = { + accordionExpanded: false, + }; + makeMenu = () => { const { intl, report } = this.props; @@ -83,8 +89,13 @@ class Report extends ImmutablePureComponent { })); } + handleAccordionToggle = setting => { + this.setState({ accordionExpanded: setting }); + } + render() { const { report } = this.props; + const { accordionExpanded } = this.state; const menu = this.makeMenu(); return ( @@ -100,7 +111,16 @@ class Report extends ImmutablePureComponent { values={{ acct: `@${report.getIn(['account', 'acct'])}` }} /> -
+
+ +
{report.get('statuses').map(status => )}
+
+
+
{report.get('content')}
— @{report.getIn(['actor', 'acct'])}
diff --git a/app/styles/components/accordion.scss b/app/styles/components/accordion.scss index 35ad3ea2e..9fea4d42e 100644 --- a/app/styles/components/accordion.scss +++ b/app/styles/components/accordion.scss @@ -20,6 +20,7 @@ text-transform: none !important; text-align: left !important; display: flex !important; + align-items: center; border: 0; width: 100%; diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index 7899634c4..ba29ef826 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -127,6 +127,7 @@ &__content { padding: 0 16px; + flex: 1; } &__title { @@ -155,4 +156,24 @@ } } } + + &__statuses .accordion { + padding: 10px; + margin-bottom: 6px; + + &__title { + font-size: 12px !important; + font-weight: normal !important; + margin-bottom: 0 !important; + } + + .status__content { + border-bottom: 1px solid var(--accent-color--med); + padding: 10px 0; + + &:last-child { + border: 0; + } + } + } } diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index 5eeda468e..e8b382a38 100644 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -72,7 +72,7 @@ button { } &.button-alternative { - color: var(--primary-text-color); + color: #fff; background: var(--brand-color); &:active, From c0770c77f577e0849fc09da6e81ca030c51aa3bf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 20:50:22 -0600 Subject: [PATCH 16/22] Reports: only display accordion when reported posts exist --- .../features/admin/components/report.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index 2f6111795..0d3a5fb2a 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -97,6 +97,8 @@ class Report extends ImmutablePureComponent { const { report } = this.props; const { accordionExpanded } = this.state; const menu = this.makeMenu(); + const statuses = report.get('statuses'); + const statusCount = statuses.count(); return (
@@ -112,13 +114,15 @@ class Report extends ImmutablePureComponent { />
- -
{report.get('statuses').map(status => )}
-
+ {statusCount > 0 && ( + +
{statuses.map(status => )}
+
+ )}
{report.get('content')}
From 8c8615498bdaf61a9410d67a5575642d18d52fa9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 21:34:07 -0600 Subject: [PATCH 17/22] Reports: parse HTML quotes --- app/soapbox/features/admin/components/report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index 0d3a5fb2a..ddbdb6f51 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -120,12 +120,12 @@ class Report extends ImmutablePureComponent { expanded={accordionExpanded} onToggle={this.handleAccordionToggle} > -
{statuses.map(status => )}
+ {statuses.map(status => )} )}
-
{report.get('content')}
+
— @{report.getIn(['actor', 'acct'])}
From 142606d43a491fb812db6d6fc0e2c9428d57335b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 22:20:31 -0600 Subject: [PATCH 18/22] Reports: refactor ReportStatus, allow deleting individual statuses --- app/soapbox/actions/admin.js | 17 +++++ .../features/admin/components/report.js | 4 +- .../admin/components/report_status.js | 68 +++++++++++++++++++ app/soapbox/selectors/index.js | 6 +- app/styles/components/admin.scss | 21 ++++-- 5 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 app/soapbox/features/admin/components/report_status.js diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 94d8b2395..84857abba 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -33,6 +33,10 @@ export const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST'; export const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS'; export const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL'; +export const ADMIN_STATUS_DELETE_REQUEST = 'ADMIN_STATUS_DELETE_REQUEST'; +export const ADMIN_STATUS_DELETE_SUCCESS = 'ADMIN_STATUS_DELETE_SUCCESS'; +export const ADMIN_STATUS_DELETE_FAIL = 'ADMIN_STATUS_DELETE_FAIL'; + export function fetchConfig() { return (dispatch, getState) => { dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); @@ -141,3 +145,16 @@ export function approveUsers(nicknames) { }); }; } + +export function deleteStatus(id) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_STATUS_DELETE_REQUEST, id }); + return api(getState) + .delete(`/api/pleroma/admin/statuses/${id}`) + .then(() => { + dispatch({ type: ADMIN_STATUS_DELETE_SUCCESS, id }); + }).catch(error => { + dispatch({ type: ADMIN_STATUS_DELETE_FAIL, error, id }); + }); + }; +} diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index ddbdb6f51..886e34a08 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -5,9 +5,9 @@ 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 StatusContent from 'soapbox/components/status_content'; 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'; @@ -120,7 +120,7 @@ class Report extends ImmutablePureComponent { expanded={accordionExpanded} onToggle={this.handleAccordionToggle} > - {statuses.map(status => )} + {statuses.map(status => )} )}
diff --git a/app/soapbox/features/admin/components/report_status.js b/app/soapbox/features/admin/components/report_status.js new file mode 100644 index 000000000..50e7ce1ec --- /dev/null +++ b/app/soapbox/features/admin/components/report_status.js @@ -0,0 +1,68 @@ +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'; + +const messages = defineMessages({ + 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; + + return [{ + text: intl.formatMessage(messages.deleteStatus, { acct: `@${status.getIn(['account', 'acct'])}` }), + action: this.handleDeleteStatus, + }]; + } + + 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 menu = this.makeMenu(); + + return ( +
+ +
+ +
+
+ ); + } + +} diff --git a/app/soapbox/selectors/index.js b/app/soapbox/selectors/index.js index d36839d6e..949c7a63d 100644 --- a/app/soapbox/selectors/index.js +++ b/app/soapbox/selectors/index.js @@ -177,11 +177,15 @@ export const makeGetChat = () => { }; export const makeGetReport = () => { + const getStatus = makeGetStatus(); + return createSelector( [ (state, id) => state.getIn(['admin', 'reports', id]), (state, id) => state.getIn(['admin', 'reports', id, 'statuses']).map( - statusId => state.getIn(['statuses', statusId])), + statusId => state.getIn(['statuses', statusId])) + .filter(s => s) + .map(s => getStatus(state, s.toJS())), ], (report, statuses) => { diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index ba29ef826..50bda569f 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -166,14 +166,25 @@ font-weight: normal !important; margin-bottom: 0 !important; } + } + + &__status { + display: flex; + border-bottom: 1px solid var(--accent-color--med); + padding: 10px 0; + + &:last-child { + border: 0; + } .status__content { - border-bottom: 1px solid var(--accent-color--med); - padding: 10px 0; + flex: 1; + padding: 0; + } - &:last-child { - border: 0; - } + &-actions { + padding: 3px 10px; + margin-left: auto; } } } From a8711f7b144850c750efd8098bf4d7fc5c4dad9f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 22:40:13 -0600 Subject: [PATCH 19/22] Reports: add "View post" button --- app/soapbox/features/admin/components/report_status.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/components/report_status.js b/app/soapbox/features/admin/components/report_status.js index 50e7ce1ec..f02e36598 100644 --- a/app/soapbox/features/admin/components/report_status.js +++ b/app/soapbox/features/admin/components/report_status.js @@ -10,6 +10,7 @@ import snackbar from 'soapbox/actions/snackbar'; import { openModal } from 'soapbox/actions/modal'; 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' }, @@ -27,9 +28,13 @@ class ReportStatus extends ImmutablePureComponent { makeMenu = () => { const { intl, status } = this.props; + const acct = status.getIn(['account', 'acct']); return [{ - text: intl.formatMessage(messages.deleteStatus, { acct: `@${status.getIn(['account', 'acct'])}` }), + text: intl.formatMessage(messages.viewStatus, { acct: `@${acct}` }), + to: `/@${acct}/posts/${status.get('id')}`, + }, { + text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }), action: this.handleDeleteStatus, }]; } From 7bd670728ded56bea931d3305bc53df71dc3fb26 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 22:46:21 -0600 Subject: [PATCH 20/22] Reports: only show quote if length > 0 --- app/soapbox/features/admin/components/report.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index 886e34a08..80f4ab339 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -125,7 +125,9 @@ class Report extends ImmutablePureComponent { )}
-
+ {report.get('content', '').length > 0 && +
+ } — @{report.getIn(['actor', 'acct'])}
From 478581cb41eec3956ff0bedb90144aece0afcca7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 22:56:17 -0600 Subject: [PATCH 21/22] Reports: add links --- app/soapbox/features/admin/components/report.js | 11 ++++++++--- app/styles/components/admin.scss | 9 +++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index 80f4ab339..d08059837 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -1,5 +1,6 @@ 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'; @@ -99,18 +100,22 @@ class Report extends ImmutablePureComponent { 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 (
- + + +

@{acct} }} />

@@ -128,7 +133,7 @@ class Report extends ImmutablePureComponent { {report.get('content', '').length > 0 &&
} - — @{report.getIn(['actor', 'acct'])} + @{reporterAcct}
diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index 50bda569f..8943e5555 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -132,6 +132,10 @@ &__title { font-weight: bold; + + a { + color: var(--primary-text-color); + } } &__quote { @@ -139,6 +143,11 @@ .byline { font-size: 12px; + + a { + color: var(--primary-text-color); + text-decoration: none; + } } } From 0e4a5101dbb37e73fbaad908ce30925b4dd50f1f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 31 Dec 2020 23:16:50 -0600 Subject: [PATCH 22/22] Reports: display media --- .../features/admin/components/report.js | 2 +- .../admin/components/report_status.js | 69 ++++++++++++++++++- app/soapbox/features/admin/reports.js | 4 +- .../features/ui/components/accordion.js | 2 +- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js index d08059837..a1e7d82bb 100644 --- a/app/soapbox/features/admin/components/report.js +++ b/app/soapbox/features/admin/components/report.js @@ -125,7 +125,7 @@ class Report extends ImmutablePureComponent { expanded={accordionExpanded} onToggle={this.handleAccordionToggle} > - {statuses.map(status => )} + {statuses.map(status => )} )}
diff --git a/app/soapbox/features/admin/components/report_status.js b/app/soapbox/features/admin/components/report_status.js index f02e36598..d98e9c755 100644 --- a/app/soapbox/features/admin/components/report_status.js +++ b/app/soapbox/features/admin/components/report_status.js @@ -8,6 +8,9 @@ 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' }, @@ -39,6 +42,66 @@ class ReportStatus extends ImmutablePureComponent { }]; } + 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 ( + + {Component => ( + + )} + + ); + } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { + const audio = status.getIn(['media_attachments', 0]); + + return ( + + {Component => ( + + )} + + ); + } else { + return ( + + {Component => } + + ); + } + } + + 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']); @@ -58,11 +121,15 @@ class ReportStatus extends ImmutablePureComponent { render() { const { status } = this.props; + const media = this.getMedia(); const menu = this.makeMenu(); return (
- +
+ + {media} +
diff --git a/app/soapbox/features/admin/reports.js b/app/soapbox/features/admin/reports.js index 55c638b64..50c7c33a7 100644 --- a/app/soapbox/features/admin/reports.js +++ b/app/soapbox/features/admin/reports.js @@ -20,7 +20,7 @@ const mapStateToProps = state => { const ids = state.getIn(['admin', 'openReports']); return { - reports: ids.map(id => getReport(state, id)), + reports: ids.toList().map(id => getReport(state, id)), }; }; @@ -57,7 +57,7 @@ class Reports extends ImmutablePureComponent { scrollKey='admin-reports' emptyMessage={intl.formatMessage(messages.emptyMessage)} > - {reports.map(report => )} + {reports.map(report => )} ); diff --git a/app/soapbox/features/ui/components/accordion.js b/app/soapbox/features/ui/components/accordion.js index 628c2912a..c38e5de14 100644 --- a/app/soapbox/features/ui/components/accordion.js +++ b/app/soapbox/features/ui/components/accordion.js @@ -12,7 +12,7 @@ export default @injectIntl class Accordion extends React.PureComponent { static propTypes = { headline: PropTypes.node.isRequired, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + children: PropTypes.oneOfType([PropTypes.string, PropTypes.element, PropTypes.node]), expanded: PropTypes.bool, onToggle: PropTypes.func, intl: PropTypes.object.isRequired,