diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js
index 85910af9d..6a93b1682 100644
--- a/app/soapbox/actions/admin.js
+++ b/app/soapbox/actions/admin.js
@@ -53,6 +53,16 @@ export const ADMIN_USERS_UNTAG_REQUEST = 'ADMIN_USERS_UNTAG_REQUEST';
export const ADMIN_USERS_UNTAG_SUCCESS = 'ADMIN_USERS_UNTAG_SUCCESS';
export const ADMIN_USERS_UNTAG_FAIL = 'ADMIN_USERS_UNTAG_FAIL';
+export const ADMIN_ADD_PERMISSION_GROUP_REQUEST = 'ADMIN_ADD_PERMISSION_GROUP_REQUEST';
+export const ADMIN_ADD_PERMISSION_GROUP_SUCCESS = 'ADMIN_ADD_PERMISSION_GROUP_SUCCESS';
+export const ADMIN_ADD_PERMISSION_GROUP_FAIL = 'ADMIN_ADD_PERMISSION_GROUP_FAIL';
+
+export const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQUEST';
+export const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
+export const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
+
+const nicknamesFromIds = (getState, ids) => ids.map(id => getState().getIn(['accounts', id, 'acct']));
+
export function fetchConfig() {
return (dispatch, getState) => {
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
@@ -208,7 +218,7 @@ export function fetchModerationLog(params) {
export function tagUsers(accountIds, tags) {
return (dispatch, getState) => {
- const nicknames = accountIds.map(id => getState().getIn(['accounts', id, 'acct']));
+ const nicknames = nicknamesFromIds(getState, accountIds);
dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags });
return api(getState)
.put('/api/v1/pleroma/admin/users/tag', { nicknames, tags })
@@ -222,7 +232,7 @@ export function tagUsers(accountIds, tags) {
export function untagUsers(accountIds, tags) {
return (dispatch, getState) => {
- const nicknames = accountIds.map(id => getState().getIn(['accounts', id, 'acct']));
+ const nicknames = nicknamesFromIds(getState, accountIds);
dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags });
return api(getState)
.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } })
@@ -233,3 +243,70 @@ export function untagUsers(accountIds, tags) {
});
};
}
+
+export function verifyUser(accountId) {
+ return (dispatch, getState) => {
+ return dispatch(tagUsers([accountId], ['verified']));
+ };
+}
+
+export function unverifyUser(accountId) {
+ return (dispatch, getState) => {
+ return dispatch(untagUsers([accountId], ['verified']));
+ };
+}
+
+export function addPermission(accountIds, permissionGroup) {
+ return (dispatch, getState) => {
+ const nicknames = nicknamesFromIds(getState, accountIds);
+ dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
+ return api(getState)
+ .post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames })
+ .then(({ data }) => {
+ dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
+ }).catch(error => {
+ dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
+ });
+ };
+}
+
+export function removePermission(accountIds, permissionGroup) {
+ return (dispatch, getState) => {
+ const nicknames = nicknamesFromIds(getState, accountIds);
+ dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
+ return api(getState)
+ .delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } })
+ .then(({ data }) => {
+ dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
+ }).catch(error => {
+ dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
+ });
+ };
+}
+
+export function promoteToAdmin(accountId) {
+ return (dispatch, getState) => {
+ return Promise.all([
+ dispatch(addPermission([accountId], 'admin')),
+ dispatch(removePermission([accountId], 'moderator')),
+ ]);
+ };
+}
+
+export function promoteToModerator(accountId) {
+ return (dispatch, getState) => {
+ return Promise.all([
+ dispatch(removePermission([accountId], 'admin')),
+ dispatch(addPermission([accountId], 'moderator')),
+ ]);
+ };
+}
+
+export function demoteToUser(accountId) {
+ return (dispatch, getState) => {
+ return Promise.all([
+ dispatch(removePermission([accountId], 'admin')),
+ dispatch(removePermission([accountId], 'moderator')),
+ ]);
+ };
+}
diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js
index aac0186c4..6aebd5e28 100644
--- a/app/soapbox/features/account/components/header.js
+++ b/app/soapbox/features/account/components/header.js
@@ -8,7 +8,13 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Icon from 'soapbox/components/icon';
import Button from 'soapbox/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { isStaff } from 'soapbox/utils/accounts';
+import {
+ isStaff,
+ isAdmin,
+ isModerator,
+ isVerified,
+ isLocal,
+} from 'soapbox/utils/accounts';
import { parseVersion } from 'soapbox/utils/features';
import classNames from 'classnames';
import Avatar from 'soapbox/components/avatar';
@@ -20,7 +26,6 @@ import { debounce } from 'lodash';
import StillImage from 'soapbox/components/still_image';
import ActionButton from 'soapbox/features/ui/components/action_button';
import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
-import { isVerified } from 'soapbox/utils/accounts';
import { openModal } from 'soapbox/actions/modal';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
@@ -54,15 +59,21 @@ const messages = defineMessages({
deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' },
verifyUser: { id: 'admin.users.actions.verify_user', defaultMessage: 'Verify @{name}' },
unverifyUser: { id: 'admin.users.actions.unverify_user', defaultMessage: 'Unverify @{name}' },
+ promoteToAdmin: { id: 'admin.users.actions.promote_to_admin', defaultMessage: 'Promote @{name} to an admin' },
+ promoteToModerator: { id: 'admin.users.actions.promote_to_moderator', defaultMessage: 'Promote @{name} to a moderator' },
+ demoteToModerator: { id: 'admin.users.actions.demote_to_moderator', defaultMessage: 'Demote @{name} to a moderator' },
+ demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' },
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
});
const mapStateToProps = state => {
const me = state.get('me');
+ const account = state.getIn(['accounts', me]);
+
return {
me,
- isStaff: isStaff(state.getIn(['accounts', me])),
+ meAccount: account,
version: parseVersion(state.getIn(['instance', 'version'])),
};
};
@@ -73,17 +84,13 @@ class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
+ meAccount: ImmutablePropTypes.map,
identity_props: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
username: PropTypes.string,
- isStaff: PropTypes.bool.isRequired,
version: PropTypes.object,
};
- static defaultProps = {
- isStaff: false,
- }
-
state = {
isSmallScreen: (window.innerWidth <= 895),
}
@@ -129,7 +136,7 @@ class Header extends ImmutablePureComponent {
}
makeMenu() {
- const { account, intl, me, isStaff, version } = this.props;
+ const { account, intl, me, meAccount, version } = this.props;
let menu = [];
@@ -200,9 +207,25 @@ class Header extends ImmutablePureComponent {
}
}
- if (isStaff) {
+ if (isStaff(meAccount)) {
menu.push(null);
- menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true });
+
+ if (isAdmin(meAccount)) {
+ menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true });
+ }
+
+ if (account.get('id') !== me && isLocal(account)) {
+ if (isAdmin(account)) {
+ menu.push({ text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }), action: this.props.onPromoteToModerator });
+ menu.push({ text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), action: this.props.onDemoteToUser });
+ } else if (isModerator(account)) {
+ menu.push({ text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), action: this.props.onPromoteToAdmin });
+ menu.push({ text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), action: this.props.onDemoteToUser });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), action: this.props.onPromoteToAdmin });
+ menu.push({ text: intl.formatMessage(messages.promoteToModerator, { name: account.get('username') }), action: this.props.onPromoteToModerator });
+ }
+ }
if (isVerified(account)) {
menu.push({ text: intl.formatMessage(messages.unverifyUser, { name: account.get('username') }), action: this.props.onUnverifyUser });
diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js
index 8e38d9fb5..b322125fd 100644
--- a/app/soapbox/features/account_timeline/components/header.js
+++ b/app/soapbox/features/account_timeline/components/header.js
@@ -104,6 +104,18 @@ export default class Header extends ImmutablePureComponent {
this.props.onUnverifyUser(this.props.account);
}
+ handlePromoteToAdmin = () => {
+ this.props.onPromoteToAdmin(this.props.account);
+ }
+
+ handlePromoteToModerator = () => {
+ this.props.onPromoteToModerator(this.props.account);
+ }
+
+ handleDemoteToUser = () => {
+ this.props.onDemoteToUser(this.props.account);
+ }
+
render() {
const { account, identity_proofs } = this.props;
const moved = (account) ? account.get('moved') : false;
@@ -132,6 +144,9 @@ export default class Header extends ImmutablePureComponent {
onDeleteUser={this.handleDeleteUser}
onVerifyUser={this.handleVerifyUser}
onUnverifyUser={this.handleUnverifyUser}
+ onPromoteToAdmin={this.handlePromoteToAdmin}
+ onPromoteToModerator={this.handlePromoteToModerator}
+ onDemoteToUser={this.handleDemoteToUser}
username={this.props.username}
/>
diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js
index c5689d158..ddcf6a3ce 100644
--- a/app/soapbox/features/account_timeline/containers/header_container.js
+++ b/app/soapbox/features/account_timeline/containers/header_container.js
@@ -12,7 +12,6 @@ import {
// unpinAccount,
subscribeAccount,
unsubscribeAccount,
-
} from '../../../actions/accounts';
import {
mentionCompose,
@@ -27,7 +26,14 @@ import { List as ImmutableList } from 'immutable';
import { getSettings } from 'soapbox/actions/settings';
import { startChat, openChat } from 'soapbox/actions/chats';
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
-import { tagUsers, untagUsers } from 'soapbox/actions/admin';
+import {
+ verifyUser,
+ unverifyUser,
+ promoteToAdmin,
+ promoteToModerator,
+ demoteToUser,
+} from 'soapbox/actions/admin';
+import { isAdmin } from 'soapbox/utils/accounts';
import snackbar from 'soapbox/actions/snackbar';
const messages = defineMessages({
@@ -37,6 +43,11 @@ const messages = defineMessages({
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
userVerified: { id: 'admin.users.user_verified_message', defaultMessage: '@{acct} was verified' },
userUnverified: { id: 'admin.users.user_unverified_message', defaultMessage: '@{acct} was unverified' },
+ promotedToAdmin: { id: 'admin.users.actions.promote_to_admin_message', defaultMessage: '@{acct} was promoted to an admin' },
+ promotedToModerator: { id: 'admin.users.actions.promote_to_moderator_message', defaultMessage: '@{acct} was promoted to a moderator' },
+ demotedToModerator: { id: 'admin.users.actions.demote_to_moderator_message', defaultMessage: '@{acct} was demoted to a moderator' },
+ demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
+
});
const isMobile = width => width <= 1190;
@@ -173,16 +184,43 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onVerifyUser(account) {
const message = intl.formatMessage(messages.userVerified, { acct: account.get('acct') });
- dispatch(tagUsers([account.get('id')], ['verified'])).then(() => {
- dispatch(snackbar.success(message));
- }).catch(() => {});
+
+ dispatch(verifyUser(account.get('id')))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
},
onUnverifyUser(account) {
const message = intl.formatMessage(messages.userUnverified, { acct: account.get('acct') });
- dispatch(untagUsers([account.get('id')], ['verified'])).then(() => {
- dispatch(snackbar.info(message));
- }).catch(() => {});
+
+ dispatch(unverifyUser(account.get('id')))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ },
+
+ onPromoteToAdmin(account) {
+ const message = intl.formatMessage(messages.promotedToAdmin, { acct: account.get('acct') });
+
+ dispatch(promoteToAdmin(account.get('id')))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ },
+
+ onPromoteToModerator(account) {
+ const messageType = isAdmin(account) ? messages.demotedToModerator : messages.promotedToModerator;
+ const message = intl.formatMessage(messageType, { acct: account.get('acct') });
+
+ dispatch(promoteToModerator(account.get('id')))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ },
+
+ onDemoteToUser(account) {
+ const message = intl.formatMessage(messages.demotedToUser, { acct: account.get('acct') });
+
+ dispatch(demoteToUser(account.get('id')))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
},
});
diff --git a/app/soapbox/features/ui/components/profile_info_panel.js b/app/soapbox/features/ui/components/profile_info_panel.js
index c32bfc42e..4b5559bfd 100644
--- a/app/soapbox/features/ui/components/profile_info_panel.js
+++ b/app/soapbox/features/ui/components/profile_info_panel.js
@@ -46,6 +46,18 @@ class ProfileInfoPanel extends ImmutablePureComponent {
displayFqn: PropTypes.bool,
};
+ getStaffBadge = () => {
+ const { account } = this.props;
+
+ if (isAdmin(account)) {
+ return