From 62d5a979392b318605aa35ba0684575af134bedf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 21:50:16 -0500 Subject: [PATCH 1/3] Add button verify/unverify a user --- app/soapbox/actions/admin.js | 36 ++++++++++++++++++ .../features/account/components/header.js | 10 +++++ .../account_timeline/components/header.js | 10 +++++ .../containers/header_container.js | 18 +++++++++ app/soapbox/features/edit_profile/index.js | 3 +- app/soapbox/reducers/accounts.js | 38 ++++++++++++++++++- app/soapbox/utils/accounts.js | 4 ++ 7 files changed, 117 insertions(+), 2 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 1667c782e..85910af9d 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -45,6 +45,14 @@ export const ADMIN_LOG_FETCH_REQUEST = 'ADMIN_LOG_FETCH_REQUEST'; export const ADMIN_LOG_FETCH_SUCCESS = 'ADMIN_LOG_FETCH_SUCCESS'; export const ADMIN_LOG_FETCH_FAIL = 'ADMIN_LOG_FETCH_FAIL'; +export const ADMIN_USERS_TAG_REQUEST = 'ADMIN_USERS_TAG_REQUEST'; +export const ADMIN_USERS_TAG_SUCCESS = 'ADMIN_USERS_TAG_SUCCESS'; +export const ADMIN_USERS_TAG_FAIL = 'ADMIN_USERS_TAG_FAIL'; + +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 function fetchConfig() { return (dispatch, getState) => { dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); @@ -197,3 +205,31 @@ export function fetchModerationLog(params) { }); }; } + +export function tagUsers(accountIds, tags) { + return (dispatch, getState) => { + const nicknames = accountIds.map(id => getState().getIn(['accounts', id, 'acct'])); + dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags }); + return api(getState) + .put('/api/v1/pleroma/admin/users/tag', { nicknames, tags }) + .then(() => { + dispatch({ type: ADMIN_USERS_TAG_SUCCESS, accountIds, tags }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_TAG_FAIL, error, accountIds, tags }); + }); + }; +} + +export function untagUsers(accountIds, tags) { + return (dispatch, getState) => { + const nicknames = accountIds.map(id => getState().getIn(['accounts', id, 'acct'])); + dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags }); + return api(getState) + .delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } }) + .then(() => { + dispatch({ type: ADMIN_USERS_UNTAG_SUCCESS, accountIds, tags }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_UNTAG_FAIL, error, accountIds, tags }); + }); + }; +} diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index e023564f7..278853097 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -19,6 +19,7 @@ import ProfileInfoPanel from '../../ui/components/profile_info_panel'; import { debounce } from 'lodash'; import StillImage from 'soapbox/components/still_image'; import ActionButton from 'soapbox/features/ui/components/action_button'; +import { isVerified } from 'soapbox/utils/accounts'; const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -48,6 +49,8 @@ const messages = defineMessages({ 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 @{name}' }, 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}' }, }); const mapStateToProps = state => { @@ -171,6 +174,13 @@ 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 }); + + if (isVerified(account)) { + menu.push({ text: intl.formatMessage(messages.unverifyUser, { name: account.get('username') }), action: this.props.onUnverifyUser }); + } else { + menu.push({ text: intl.formatMessage(messages.verifyUser, { name: account.get('username') }), action: this.props.onVerifyUser }); + } + menu.push({ text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }), action: this.props.onDeactivateUser }); menu.push({ text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }), action: this.props.onDeleteUser }); } diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index 86798d3ab..a16053fc9 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -92,6 +92,14 @@ export default class Header extends ImmutablePureComponent { this.props.onDeleteUser(this.props.account); } + handleVerifyUser = () => { + this.props.onVerifyUser(this.props.account); + } + + handleUnverifyUser = () => { + this.props.onUnverifyUser(this.props.account); + } + render() { const { account, identity_proofs } = this.props; const moved = (account) ? account.get('moved') : false; @@ -117,6 +125,8 @@ export default class Header extends ImmutablePureComponent { onAddToList={this.handleAddToList} onDeactivateUser={this.handleDeactivateUser} onDeleteUser={this.handleDeleteUser} + onVerifyUser={this.handleVerifyUser} + onUnverifyUser={this.handleUnverifyUser} 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 8d6438894..fdf0b19e3 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -25,12 +25,16 @@ 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'; +import { tagUsers, untagUsers } from 'soapbox/actions/admin'; +import snackbar from 'soapbox/actions/snackbar'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, 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' }, }); const makeMapStateToProps = () => { @@ -154,6 +158,20 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onDeleteUser(account) { dispatch(deleteUserModal(intl, account.get('id'))); }, + + onVerifyUser(account) { + const message = intl.formatMessage(messages.userVerified, { acct: account.get('acct') }); + dispatch(tagUsers([account.get('id')], ['verified'])).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(() => {}); + }, }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/soapbox/features/edit_profile/index.js b/app/soapbox/features/edit_profile/index.js index 643c19c0f..110b55c6e 100644 --- a/app/soapbox/features/edit_profile/index.js +++ b/app/soapbox/features/edit_profile/index.js @@ -21,6 +21,7 @@ import { } from 'immutable'; import { patchMe } from 'soapbox/actions/me'; import { unescape } from 'lodash'; +import { isVerified } from 'soapbox/utils/accounts'; const messages = defineMessages({ heading: { id: 'column.edit_profile', defaultMessage: 'Edit profile' }, @@ -161,7 +162,7 @@ class EditProfile extends ImmutablePureComponent { render() { const { intl, maxFields, account } = this.props; - const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified'); + const verified = isVerified(account); return ( diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js index f8f869ad5..5a908941d 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.js @@ -6,8 +6,18 @@ import { import { CHATS_FETCH_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats'; import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; import { normalizeAccount as normalizeAccount2 } from 'soapbox/actions/importer/normalizer'; -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { + Map as ImmutableMap, + List as ImmutableList, + fromJS, +} from 'immutable'; import { normalizePleromaUserFields } from 'soapbox/utils/pleroma'; +import { + ADMIN_USERS_TAG_REQUEST, + ADMIN_USERS_TAG_FAIL, + ADMIN_USERS_UNTAG_REQUEST, + ADMIN_USERS_UNTAG_FAIL, +} from 'soapbox/actions/admin'; const initialState = ImmutableMap(); @@ -43,6 +53,26 @@ const importAccountsFromChats = (state, chats) => state.withMutations(mutable => chats.forEach(chat => importAccountFromChat(mutable, chat))); +const addTags = (state, accountIds, tags) => { + return state.withMutations(state => { + accountIds.forEach(id => { + state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => + v.toOrderedSet().union(tags).toList(), + ); + }); + }); +}; + +const removeTags = (state, accountIds, tags) => { + return state.withMutations(state => { + accountIds.forEach(id => { + state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => + v.toOrderedSet().subtract(tags).toList(), + ); + }); + }); +}; + export default function accounts(state = initialState, action) { switch(action.type) { case ACCOUNT_IMPORT: @@ -58,6 +88,12 @@ export default function accounts(state = initialState, action) { case CHAT_FETCH_SUCCESS: case STREAMING_CHAT_UPDATE: return importAccountsFromChats(state, [action.chat]); + case ADMIN_USERS_TAG_REQUEST: + case ADMIN_USERS_UNTAG_FAIL: + return addTags(state, action.accountIds, action.tags); + case ADMIN_USERS_UNTAG_REQUEST: + case ADMIN_USERS_TAG_FAIL: + return removeTags(state, action.accountIds, action.tags); default: return state; } diff --git a/app/soapbox/utils/accounts.js b/app/soapbox/utils/accounts.js index acc4cde65..7e25f868c 100644 --- a/app/soapbox/utils/accounts.js +++ b/app/soapbox/utils/accounts.js @@ -45,3 +45,7 @@ export const isLocal = account => { let domain = account.get('acct').split('@')[1]; return domain === undefined ? true : false; }; + +export const isVerified = account => ( + account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified') +); From e4751bef9a4525758ff75d0b2b35e86d9b0b15b3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 21:57:24 -0500 Subject: [PATCH 2/3] Change display of moderation buttons for own user --- app/soapbox/features/account/components/header.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 278853097..373f119c0 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -171,7 +171,7 @@ class Header extends ImmutablePureComponent { } } - if (account.get('id') !== me && isStaff) { + if (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 }); @@ -181,8 +181,10 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.verifyUser, { name: account.get('username') }), action: this.props.onVerifyUser }); } - menu.push({ text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }), action: this.props.onDeactivateUser }); - menu.push({ text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }), action: this.props.onDeleteUser }); + if (account.get('id') !== me) { + menu.push({ text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }), action: this.props.onDeactivateUser }); + menu.push({ text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }), action: this.props.onDeleteUser }); + } } return menu; From f6de89ca50532c09db74aff58b4001ee9cbd4a8c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 22:23:33 -0500 Subject: [PATCH 3/3] Make editing display names of verified users configurable --- app/soapbox/actions/soapbox.js | 1 + app/soapbox/features/edit_profile/index.js | 13 +++++++++---- app/soapbox/features/soapbox_config/index.js | 10 ++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 0fc0a3382..023c4fccf 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -39,6 +39,7 @@ export const defaultConfig = ImmutableMap({ homeFooter: ImmutableList(), }), allowedEmoji: allowedEmoji, + verifiedCanEditName: false, }); export function getSoapboxConfig(state) { diff --git a/app/soapbox/features/edit_profile/index.js b/app/soapbox/features/edit_profile/index.js index 110b55c6e..566e9ee3e 100644 --- a/app/soapbox/features/edit_profile/index.js +++ b/app/soapbox/features/edit_profile/index.js @@ -22,6 +22,7 @@ import { import { patchMe } from 'soapbox/actions/me'; import { unescape } from 'lodash'; import { isVerified } from 'soapbox/utils/accounts'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const messages = defineMessages({ heading: { id: 'column.edit_profile', defaultMessage: 'Edit profile' }, @@ -32,9 +33,11 @@ const messages = defineMessages({ const mapStateToProps = state => { const me = state.get('me'); + const soapbox = getSoapboxConfig(state); return { account: state.getIn(['accounts', me]), maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4), + verifiedCanEditName: soapbox.get('verifiedCanEditName'), }; }; @@ -61,6 +64,7 @@ class EditProfile extends ImmutablePureComponent { intl: PropTypes.object.isRequired, account: ImmutablePropTypes.map, maxFields: PropTypes.number, + verifiedCanEditName: PropTypes.bool, }; state = { @@ -161,8 +165,9 @@ class EditProfile extends ImmutablePureComponent { } render() { - const { intl, maxFields, account } = this.props; + const { intl, maxFields, account, verifiedCanEditName } = this.props; const verified = isVerified(account); + const canEditName = verifiedCanEditName || !verified; return ( @@ -170,13 +175,13 @@ class EditProfile extends ImmutablePureComponent {
} name='display_name' value={this.state.display_name} onChange={this.handleTextChange} - disabled={verified} - hint={verified && intl.formatMessage(messages.verified)} + disabled={!canEditName} + hint={!canEditName && intl.formatMessage(messages.verified)} /> } diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index e06520c63..21744cc10 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -12,6 +12,7 @@ import { SimpleTextarea, FileChooserLogo, FormPropTypes, + Checkbox, } from 'soapbox/features/forms'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { updateConfig } from 'soapbox/actions/admin'; @@ -39,6 +40,7 @@ const messages = defineMessages({ customCssLabel: { id: 'soapbox_config.custom_css.meta_fields.url_placeholder', defaultMessage: 'URL' }, rawJSONLabel: { id: 'soapbox_config.raw_json_label', defaultMessage: 'Advanced: Edit raw JSON data' }, rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Edit the settings data directly. Changes made directly to the JSON file will override the form fields above. Click "Save" to apply your changes.' }, + verifiedCanEditNameLabel: { id: 'soapbox_config.verified_can_edit_name_label', defaultMessage: 'Allow verified users to edit their own display name.' }, }); const listenerOptions = supportsPassiveEvents ? { passive: true } : false; @@ -232,6 +234,14 @@ class SoapboxConfig extends ImmutablePureComponent { onChange={this.handleChange(['copyright'], (e) => e.target.value)} /> + + e.target.checked)} + /> +