Merge branch 'admin-improvements' into 'develop'

Admin improvements

See merge request soapbox-pub/soapbox-fe!424
This commit is contained in:
Alex Gleason 2021-01-18 22:16:28 +00:00
commit 5b5397312b
14 changed files with 215 additions and 59 deletions

View File

@ -1,5 +1,5 @@
import api from '../api';
import { importFetchedStatuses } from 'soapbox/actions/importer';
import { importFetchedAccount, 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';
@ -73,7 +73,11 @@ export function fetchReports(params) {
return api(getState)
.get('/api/pleroma/admin/reports', { params })
.then(({ data: { reports } }) => {
reports.forEach(report => dispatch(importFetchedStatuses(report.statuses)));
reports.forEach(report => {
dispatch(importFetchedAccount(report.account));
dispatch(importFetchedAccount(report.actor));
dispatch(importFetchedStatuses(report.statuses));
});
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
}).catch(error => {
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });

View File

@ -0,0 +1,72 @@
import { defineMessages } from 'react-intl';
import { openModal } from 'soapbox/actions/modal';
import { deactivateUsers, deleteUsers, deleteStatus } from 'soapbox/actions/admin';
import snackbar from 'soapbox/actions/snackbar';
const messages = defineMessages({
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.users.user_deactivated_message', defaultMessage: '{acct} was deactivated' },
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.users.user_deleted_message', defaultMessage: '{acct} was deleted' },
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.statuses.status_deleted_message', defaultMessage: 'Post by {acct} was deleted' },
});
export function deactivateUserModal(intl, accountId, afterConfirm = () => {}) {
return function(dispatch, getState) {
const state = getState();
const acct = state.getIn(['accounts', accountId, 'acct']);
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deactivateUserPrompt, { acct: `@${acct}` }),
confirm: intl.formatMessage(messages.deactivateUserConfirm, { acct: `@${acct}` }),
onConfirm: () => {
dispatch(deactivateUsers([acct])).then(() => {
const message = intl.formatMessage(messages.userDeactivated, { acct: `@${acct}` });
dispatch(snackbar.success(message));
afterConfirm();
}).catch(() => {});
},
}));
};
}
export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
return function(dispatch, getState) {
const state = getState();
const acct = state.getIn(['accounts', accountId, 'acct']);
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteUserPrompt, { acct: `@${acct}` }),
confirm: intl.formatMessage(messages.deleteUserConfirm, { acct: `@${acct}` }),
onConfirm: () => {
dispatch(deleteUsers([acct])).then(() => {
const message = intl.formatMessage(messages.userDeleted, { acct: `@${acct}` });
dispatch(snackbar.success(message));
afterConfirm();
}).catch(() => {});
},
}));
};
}
export function deleteStatusModal(intl, statusId, afterConfirm = () => {}) {
return function(dispatch, getState) {
const state = getState();
const accountId = state.getIn(['statuses', statusId, 'account']);
const acct = state.getIn(['accounts', accountId, 'acct']);
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteStatusPrompt, { acct: `@${acct}` }),
confirm: intl.formatMessage(messages.deleteStatusConfirm),
onConfirm: () => {
dispatch(deleteStatus(statusId)).then(() => {
const message = intl.formatMessage(messages.statusDeleted, { acct: `@${acct}` });
dispatch(snackbar.success(message));
}).catch(() => {});
afterConfirm();
},
}));
};
}

View File

@ -45,6 +45,9 @@ const messages = defineMessages({
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove post from group' },
deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' },
deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete {acct}' },
deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' },
});
class StatusActionBar extends ImmutablePureComponent {
@ -67,6 +70,9 @@ class StatusActionBar extends ImmutablePureComponent {
onBlock: PropTypes.func,
onReport: PropTypes.func,
onEmbed: PropTypes.func,
onDeactivateUser: PropTypes.func,
onDeleteUser: PropTypes.func,
onDeleteStatus: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
withDismiss: PropTypes.bool,
@ -242,6 +248,18 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onGroupRemoveStatus(status.getIn(['group', 'id']), status.get('id'));
}
handleDeactivateUser = () => {
this.props.onDeactivateUser(this.props.status);
}
handleDeleteUser = () => {
this.props.onDeleteUser(this.props.status);
}
handleDeleteStatus = () => {
this.props.onDeleteStatus(this.props.status);
}
_makeMenu = (publicStatus) => {
const { status, intl, withDismiss, withGroupAdmin, me, isStaff } = this.props;
const mutingConversation = status.get('muted');
@ -291,6 +309,9 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/` });
// menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
menu.push({ text: intl.formatMessage(messages.deactivateUser, { acct: `@${status.getIn(['account', 'acct'])}` }), action: this.handleDeactivateUser });
menu.push({ text: intl.formatMessage(messages.deleteUser, { acct: `@${status.getIn(['account', 'acct'])}` }), action: this.handleDeleteUser });
menu.push({ text: intl.formatMessage(messages.deleteStatus), action: this.handleDeleteStatus });
}
if (withGroupAdmin) {

View File

@ -36,6 +36,7 @@ import {
} from '../actions/groups';
import { getSettings } from '../actions/settings';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { deactivateUserModal, deleteUserModal, deleteStatusModal } from 'soapbox/actions/moderation';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -208,6 +209,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(groupRemoveStatus(groupId, statusId));
},
onDeactivateUser(status) {
dispatch(deactivateUserModal(intl, status.getIn(['account', 'id'])));
},
onDeleteUser(status) {
dispatch(deleteUserModal(intl, status.getIn(['account', 'id'])));
},
onDeleteStatus(status) {
dispatch(deleteStatusModal(intl, status.get('id')));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

View File

@ -46,6 +46,8 @@ const messages = defineMessages({
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' },
deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete {acct}' },
});
const mapStateToProps = state => {
@ -169,6 +171,8 @@ class Header extends ImmutablePureComponent {
if (account.get('id') !== me && isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true });
menu.push({ text: intl.formatMessage(messages.deactivateUser, { acct: `@${account.get('acct')}` }), action: this.props.onDeactivateUser });
menu.push({ text: intl.formatMessage(messages.deleteUser, { acct: `@${account.get('acct')}` }), action: this.props.onDeleteUser });
}
return menu;

View File

@ -84,6 +84,14 @@ export default class Header extends ImmutablePureComponent {
this.props.onAddToList(this.props.account);
}
handleDeactivateUser = () => {
this.props.onDeactivateUser(this.props.account);
}
handleDeleteUser = () => {
this.props.onDeleteUser(this.props.account);
}
render() {
const { account, identity_proofs } = this.props;
const moved = (account) ? account.get('moved') : false;
@ -107,6 +115,8 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
onDeactivateUser={this.handleDeactivateUser}
onDeleteUser={this.handleDeleteUser}
username={this.props.username}
/>
</div>

View File

@ -24,6 +24,7 @@ import { List as ImmutableList } from 'immutable';
import { getSettings } from 'soapbox/actions/settings';
import { startChat, openChat } from 'soapbox/actions/chats';
import { isMobile } from 'soapbox/is_mobile';
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@ -145,6 +146,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
}).catch(() => {});
},
onDeactivateUser(account) {
dispatch(deactivateUserModal(intl, account.get('id')));
},
onDeleteUser(account) {
dispatch(deleteUserModal(intl, account.get('id')));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

View File

@ -9,20 +9,14 @@ 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 { closeReports } from 'soapbox/actions/admin';
import snackbar from 'soapbox/actions/snackbar';
import { openModal } from 'soapbox/actions/modal';
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
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' },
deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' },
deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete {acct}' },
});
export default @connect()
@ -60,34 +54,14 @@ class Report extends ImmutablePureComponent {
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();
},
}));
const accountId = report.getIn(['account', 'id']);
dispatch(deactivateUserModal(intl, accountId, () => 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();
},
}));
const accountId = report.getIn(['account', 'id']);
dispatch(deleteUserModal(intl, accountId, () => this.handleCloseReport()));
}
handleAccordionToggle = setting => {

View File

@ -5,19 +5,15 @@ 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';
import { deleteStatusModal } from 'soapbox/actions/moderation';
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' },
deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' },
});
export default @connect()
@ -104,19 +100,8 @@ class ReportStatus extends ImmutablePureComponent {
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();
},
}));
dispatch(deleteStatusModal(intl, statusId));
}
render() {

View File

@ -36,6 +36,9 @@ const messages = defineMessages({
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate {acct}' },
deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete {acct}' },
deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' },
});
const mapStateToProps = state => {
@ -74,6 +77,9 @@ class ActionBar extends React.PureComponent {
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
onDeactivateUser: PropTypes.func,
onDeleteUser: PropTypes.func,
onDeleteStatus: PropTypes.func,
intl: PropTypes.object.isRequired,
onOpenUnauthorizedModal: PropTypes.func.isRequired,
me: SoapboxPropTypes.me,
@ -221,6 +227,18 @@ class ActionBar extends React.PureComponent {
}
}
handleDeactivateUser = () => {
this.props.onDeactivateUser(this.props.status);
}
handleDeleteUser = () => {
this.props.onDeleteUser(this.props.status);
}
handleDeleteStatus = () => {
this.props.onDeleteStatus(this.props.status);
}
setRef = c => {
this.node = c;
}
@ -276,6 +294,9 @@ class ActionBar extends React.PureComponent {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/` });
// menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
menu.push({ text: intl.formatMessage(messages.deactivateUser, { acct: `@${status.getIn(['account', 'acct'])}` }), action: this.handleDeactivateUser });
menu.push({ text: intl.formatMessage(messages.deleteUser, { acct: `@${status.getIn(['account', 'acct'])}` }), action: this.handleDeleteUser });
menu.push({ text: intl.formatMessage(messages.deleteStatus), action: this.handleDeleteStatus });
}
}

View File

@ -31,6 +31,7 @@ import { openModal } from '../../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { showAlertForError } from '../../../actions/alerts';
import { getSettings } from 'soapbox/actions/settings';
import { deactivateUserModal, deleteUserModal, deleteStatusModal } from 'soapbox/actions/moderation';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -190,6 +191,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onDeactivateUser(status) {
dispatch(deactivateUserModal(intl, status.getIn(['account', 'id'])));
},
onDeleteUser(status) {
dispatch(deleteUserModal(intl, status.getIn(['account', 'id'])));
},
onDeleteStatus(status) {
dispatch(deleteStatusModal(intl, status.get('id')));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));

View File

@ -47,6 +47,7 @@ import { textForScreenReader, defaultMediaVisibility } from '../../components/st
import Icon from 'soapbox/components/icon';
import { getSettings } from 'soapbox/actions/settings';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { deactivateUserModal, deleteUserModal, deleteStatusModal } from 'soapbox/actions/moderation';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -302,6 +303,21 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
}
handleDeactivateUser = (status) => {
const { dispatch, intl } = this.props;
dispatch(deactivateUserModal(intl, status.getIn(['account', 'id'])));
}
handleDeleteUser = (status) => {
const { dispatch, intl } = this.props;
dispatch(deleteUserModal(intl, status.getIn(['account', 'id'])));
}
handleDeleteStatus = (status) => {
const { dispatch, intl } = this.props;
dispatch(deleteStatusModal(intl, status.get('id')));
}
handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
}
@ -523,6 +539,9 @@ class Status extends ImmutablePureComponent {
onPin={this.handlePin}
onBookmark={this.handleBookmark}
onEmbed={this.handleEmbed}
onDeactivateUser={this.handleDeactivateUser}
onDeleteUser={this.handleDeleteUser}
onDeleteStatus={this.handleDeleteStatus}
allowedEmoji={this.props.allowedEmoji}
/>
</div>

View File

@ -68,11 +68,11 @@
"admin.dashcounters.user_count_label": "użytkownicy",
"admin.dashwidgets.software_header": "Oprogramowanie",
"admin.moderation_log.empty_message": "Nie wykonałeś(-aś) jeszcze żadnych działań moderacyjnych. Kiedy jakieś zostaną wykonane, ich historia pojawi się tutaj.",
"admin.reports.actions.deactivate_user": "Dezaktywuj {acct}",
"admin.reports.actions.delete_user": "Usuń {acct}",
"admin.reports.actions.delete_sstatus": "Usuń wpis",
"admin.reports.actions.user_deactivated_message": "Zdezaktywowano {acct}",
"admin.reports.actions.user_deleted_message": "Usunięto {acct}",
"admin.users.actions.deactivate_user": "Dezaktywuj {acct}",
"admin.users.actions.delete_user": "Usuń {acct}",
"admin.statuses.actions.delete_status": "Usuń wpis",
"admin.users.actions.user_deactivated_message": "Zdezaktywowano {acct}",
"admin.users.actions.user_deleted_message": "Usunięto {acct}",
"admin.reports.actions.view_status": "Wyświetl wpis",
"admin.reports.empty_message": "Brak otwartych zgłoszeń. Gdy użytkownik zostanie zgłoszony, pojawi się on tutaj.",
"admin.reports.report_closed_message": "Zamknięto zgłoszenie dotyczące {acct}",

View File

@ -131,10 +131,13 @@
&__content {
padding: 0 16px;
flex: 1;
overflow: hidden;
}
&__title {
font-weight: bold;
text-overflow: ellipsis;
overflow: hidden;
a {
color: var(--primary-text-color);
@ -144,6 +147,10 @@
&__quote {
font-size: 14px;
a {
color: var(--brand-color--hicontrast);
}
.byline {
font-size: 12px;
@ -180,6 +187,10 @@
}
}
&__status-content {
overflow: hidden;
}
&__status {
display: flex;
border-bottom: 1px solid var(--accent-color--med);