From 3120cc845399529d9d104adf24dbe3a89f44ec28 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 11:25:48 -0500 Subject: [PATCH 01/29] AccountModerationModal: boilerplate --- .../features/account/components/header.tsx | 11 +++-- .../features/ui/components/modal_root.js | 2 + .../modals/account-moderation-modal.tsx | 47 +++++++++++++++++++ .../features/ui/util/async-components.ts | 4 ++ app/soapbox/locales/en.json | 2 +- 5 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 app/soapbox/features/ui/components/modals/account-moderation-modal.tsx diff --git a/app/soapbox/features/account/components/header.tsx b/app/soapbox/features/account/components/header.tsx index c9620485b..4691287d0 100644 --- a/app/soapbox/features/account/components/header.tsx +++ b/app/soapbox/features/account/components/header.tsx @@ -59,7 +59,7 @@ const messages = defineMessages({ endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, removeFromFollowers: { id: 'account.remove_from_followers', defaultMessage: 'Remove this follower' }, - admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, + adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{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 @{name}' }, deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' }, @@ -287,6 +287,10 @@ const Header: React.FC = ({ account }) => { .catch(() => {}); }; + const onModerate = () => { + dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id })); + }; + const onRemoveFromFollowers = () => { dispatch((_, getState) => { const unfollowModal = getSettings(getState()).get('unfollowModal'); @@ -534,9 +538,8 @@ const Header: React.FC = ({ account }) => { if (ownAccount?.admin) { menu.push({ - text: intl.formatMessage(messages.admin_account, { name: account.username }), - to: `/pleroma/admin/#/users/${account.id}/`, - newTab: true, + text: intl.formatMessage(messages.adminAccount, { name: account.username }), + action: onModerate, icon: require('@tabler/icons/gavel.svg'), }); } diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index f7b9b007c..2c2de0530 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -32,6 +32,7 @@ import { CompareHistoryModal, VerifySmsModal, FamiliarFollowersModal, + AccountModerationModal, } from 'soapbox/features/ui/util/async-components'; import BundleContainer from '../containers/bundle_container'; @@ -69,6 +70,7 @@ const MODAL_COMPONENTS = { 'COMPARE_HISTORY': CompareHistoryModal, 'VERIFY_SMS': VerifySmsModal, 'FAMILIAR_FOLLOWERS': FamiliarFollowersModal, + 'ACCOUNT_MODERATION': AccountModerationModal, }; export default class ModalRoot extends React.PureComponent { diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx new file mode 100644 index 000000000..2d338da3c --- /dev/null +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import MissingIndicator from 'soapbox/components/missing_indicator'; +import { Modal } from 'soapbox/components/ui'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; + +const messages = defineMessages({ + title: { id: 'account_moderation_modal.title', defaultMessage: 'Moderate @{acct}' }, +}); + +const getAccount = makeGetAccount(); + +interface IAccountModerationModal { + /** Action to close the modal. */ + onClose: (type: string) => void, + /** ID of the account to moderate. */ + accountId: string, +} + +/** Moderator actions against accounts. */ +const AccountModerationModal: React.FC = ({ onClose, accountId }) => { + const intl = useIntl(); + const account = useAppSelector(state => getAccount(state, accountId)); + + const handleClose = () => onClose('ACCOUNT_MODERATION'); + + if (!account) { + return ( + + + + ); + } + + return ( + +
TODO
+
+ ); +}; + +export default AccountModerationModal; diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index 43b5c16a1..979c5da7d 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -106,6 +106,10 @@ export function ReportModal() { return import(/* webpackChunkName: "modals/report-modal/report-modal" */'../components/modals/report-modal/report-modal'); } +export function AccountModerationModal() { + return import(/* webpackChunkName: "modals/account-moderation-modal" */'../components/modals/account-moderation-modal'); +} + export function MediaGallery() { return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery'); } diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index cded97f64..7ce350976 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -974,7 +974,7 @@ "soapbox_config.single_user_mode_profile_label": "Main user handle", "soapbox_config.verified_can_edit_name_label": "Allow verified users to edit their own display name.", "status.actions.more": "More", - "status.admin_account": "Open moderation interface for @{name}", + "status.admin_account": "Moderate @{name}", "status.admin_status": "Open this post in the moderation interface", "status.block": "Block @{name}", "status.bookmark": "Bookmark", From 774894e1278475514bd10538020dfb8bb5412892 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 11:46:08 -0500 Subject: [PATCH 02/29] AccountModerationModal: add "Open in AdminFE" button --- .../modals/account-moderation-modal.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx index 2d338da3c..0aca9b2f5 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx @@ -1,15 +1,11 @@ import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import MissingIndicator from 'soapbox/components/missing_indicator'; -import { Modal } from 'soapbox/components/ui'; +import { Button, HStack, Modal } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; -const messages = defineMessages({ - title: { id: 'account_moderation_modal.title', defaultMessage: 'Moderate @{acct}' }, -}); - const getAccount = makeGetAccount(); interface IAccountModerationModal { @@ -21,7 +17,6 @@ interface IAccountModerationModal { /** Moderator actions against accounts. */ const AccountModerationModal: React.FC = ({ onClose, accountId }) => { - const intl = useIntl(); const account = useAppSelector(state => getAccount(state, accountId)); const handleClose = () => onClose('ACCOUNT_MODERATION'); @@ -34,12 +29,20 @@ const AccountModerationModal: React.FC = ({ onClose, ac ); } + const handleAdminFE = () => { + window.open(`/pleroma/admin/#/users/${account.id}/`, '_blank'); + }; + return ( } onClose={handleClose} > -
TODO
+ + +
); }; From 1e3a959c1c6b8cabbb4018851bf29c3a5dc49f80 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 11:48:37 -0500 Subject: [PATCH 03/29] AccountModerationModal: put AdminFE behind a feature flag --- .../modals/account-moderation-modal.tsx | 15 +++++++++------ app/soapbox/utils/features.ts | 6 ++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx index 0aca9b2f5..47b321d99 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx @@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl'; import MissingIndicator from 'soapbox/components/missing_indicator'; import { Button, HStack, Modal } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; +import { useAppSelector, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; const getAccount = makeGetAccount(); @@ -17,6 +17,7 @@ interface IAccountModerationModal { /** Moderator actions against accounts. */ const AccountModerationModal: React.FC = ({ onClose, accountId }) => { + const features = useFeatures(); const account = useAppSelector(state => getAccount(state, accountId)); const handleClose = () => onClose('ACCOUNT_MODERATION'); @@ -38,11 +39,13 @@ const AccountModerationModal: React.FC = ({ onClose, ac title={} onClose={handleClose} > - - - + {features.adminFE && ( + + + + )} ); }; diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 78908b04f..f220f5e67 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -142,6 +142,12 @@ const getInstanceFeatures = (instance: Instance) => { */ accountWebsite: v.software === TRUTHSOCIAL, + /** + * An additional moderator interface is available on the domain. + * @see /pleroma/admin + */ + adminFE: v.software === PLEROMA, + /** * Can display announcements set by admins. * @see GET /api/v1/announcements From d653c451b6ec6152ab997bae08df477c8a1b8437 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 12:33:33 -0500 Subject: [PATCH 04/29] AccountModerationModal: add account preview --- .../modals/account-moderation-modal.tsx | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx index 47b321d99..84c7c824d 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; +import Account from 'soapbox/components/account'; import MissingIndicator from 'soapbox/components/missing_indicator'; -import { Button, HStack, Modal } from 'soapbox/components/ui'; +import { Button, HStack, Modal, Stack } from 'soapbox/components/ui'; import { useAppSelector, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; @@ -39,13 +40,24 @@ const AccountModerationModal: React.FC = ({ onClose, ac title={} onClose={handleClose} > - {features.adminFE && ( - - - - )} + +
+ +
+ + {features.adminFE && ( + + + + )} +
); }; From cfdace94544ca284df59abae13baaa58c736d45e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 13:19:08 -0500 Subject: [PATCH 05/29] AccountModerationModal: add staff role picker --- app/soapbox/actions/admin.ts | 13 +++ .../modals/account-moderation-modal.tsx | 81 ++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts index 660b52dce..e486ca873 100644 --- a/app/soapbox/actions/admin.ts +++ b/app/soapbox/actions/admin.ts @@ -476,6 +476,18 @@ const demoteToUser = (accountId: string) => dispatch(removePermission([accountId], 'moderator')), ]); +const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') => + (dispatch: AppDispatch) => { + switch (role) { + case 'user': + return dispatch(demoteToUser(accountId)); + case 'moderator': + return dispatch(promoteToModerator(accountId)); + case 'admin': + return dispatch(promoteToAdmin(accountId)); + } + }; + const suggestUsers = (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { const nicknames = nicknamesFromIds(getState, accountIds); @@ -576,6 +588,7 @@ export { promoteToAdmin, promoteToModerator, demoteToUser, + setRole, suggestUsers, unsuggestUsers, }; diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx index 84c7c824d..00bcdc7f8 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal.tsx @@ -1,14 +1,30 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import React, { useMemo } from 'react'; +import { defineMessages, FormattedMessage, useIntl, MessageDescriptor } from 'react-intl'; +import { setRole } from 'soapbox/actions/admin'; +import snackbar from 'soapbox/actions/snackbar'; import Account from 'soapbox/components/account'; +import List, { ListItem } from 'soapbox/components/list'; import MissingIndicator from 'soapbox/components/missing_indicator'; import { Button, HStack, Modal, Stack } from 'soapbox/components/ui'; -import { useAppSelector, useFeatures } from 'soapbox/hooks'; +import { SelectDropdown } from 'soapbox/features/forms'; +import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; +import type { Account as AccountEntity } from 'soapbox/types/entities'; + const getAccount = makeGetAccount(); +const messages = defineMessages({ + roleUser: { id: 'account_moderation_modal.roles.user', defaultMessage: 'User' }, + roleModerator: { id: 'account_moderation_modal.roles.moderator', defaultMessage: 'Moderator' }, + roleAdmin: { id: 'account_moderation_modal.roles.admin', defaultMessage: 'Admin' }, + 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' }, +}); + interface IAccountModerationModal { /** Action to close the modal. */ onClose: (type: string) => void, @@ -16,11 +32,34 @@ interface IAccountModerationModal { accountId: string, } +/** Staff role. */ +type AccountRole = 'user' | 'moderator' | 'admin'; + +/** Get the highest staff role associated with the account. */ +const getRole = (account: AccountEntity): AccountRole => { + if (account.admin) { + return 'admin'; + } else if (account.moderator) { + return 'moderator'; + } else { + return 'user'; + } +}; + /** Moderator actions against accounts. */ const AccountModerationModal: React.FC = ({ onClose, accountId }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const features = useFeatures(); const account = useAppSelector(state => getAccount(state, accountId)); + const roles: Record = useMemo(() => ({ + user: intl.formatMessage(messages.roleUser), + moderator: intl.formatMessage(messages.roleModerator), + admin: intl.formatMessage(messages.roleAdmin), + }), []); + const handleClose = () => onClose('ACCOUNT_MODERATION'); if (!account) { @@ -35,6 +74,32 @@ const AccountModerationModal: React.FC = ({ onClose, ac window.open(`/pleroma/admin/#/users/${account.id}/`, '_blank'); }; + const handleRoleChange: React.ChangeEventHandler = (e) => { + const role = e.target.value as AccountRole; + + dispatch(setRole(account.id, role)) + .then(() => { + let message: MessageDescriptor | undefined; + + if (role === 'admin') { + message = messages.promotedToAdmin; + } else if (role === 'moderator' && account.admin) { + message = messages.demotedToModerator; + } else if (role === 'moderator') { + message = messages.promotedToModerator; + } else if (role === 'user') { + message = messages.demotedToUser; + } + + if (message) { + dispatch(snackbar.success(intl.formatMessage(message, { acct: account.acct }))); + } + }) + .catch(() => {}); + }; + + const accountRole = getRole(account); + return ( } @@ -50,6 +115,16 @@ const AccountModerationModal: React.FC = ({ onClose, ac /> + + }> + + + + {features.adminFE && ( + + + diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal/badge-input.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal/badge-input.tsx new file mode 100644 index 000000000..63ef22fad --- /dev/null +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal/badge-input.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useIntl, defineMessages } from 'react-intl'; + +import { TagInput } from 'soapbox/components/ui'; +import { badgeToTag, tagToBadge } from 'soapbox/utils/badges'; + +const messages = defineMessages({ + placeholder: { id: 'badge_input.placeholder', defaultMessage: 'Enter a badge…' }, +}); + +interface IBadgeInput { + /** A badge is a tag that begins with `badge:` */ + badges: string[], + /** Callback when badges change. */ + onChange: (badges: string[]) => void, +} + +/** Manages user badges. */ +const BadgeInput: React.FC = ({ badges, onChange }) => { + const intl = useIntl(); + const tags = badges.map(badgeToTag); + + const handleTagsChange = (tags: string[]) => { + const badges = tags.map(tagToBadge); + onChange(badges); + }; + + return ( + + ); +}; + +export default BadgeInput; \ No newline at end of file diff --git a/app/soapbox/utils/badges.ts b/app/soapbox/utils/badges.ts new file mode 100644 index 000000000..139537f6f --- /dev/null +++ b/app/soapbox/utils/badges.ts @@ -0,0 +1,47 @@ +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; + +import type { Account } from 'soapbox/types/entities'; + +/** Convert a plain tag into a badge. */ +const tagToBadge = (tag: string) => `badge:${tag}`; + +/** Convert a badge into a plain tag. */ +const badgeToTag = (badge: string) => badge.replace(/^badge:/, ''); + +/** Difference between an old and new set of tags. */ +interface TagDiff { + /** New tags that were added. */ + added: string[], + /** Old tags that were removed. */ + removed: string[], +} + +/** Returns the differences between two sets of tags. */ +const getTagDiff = (oldTags: string[], newTags: string[]): TagDiff => { + const o = ImmutableOrderedSet(oldTags); + const n = ImmutableOrderedSet(newTags); + + return { + added: n.subtract(o).toArray(), + removed: o.subtract(n).toArray(), + }; +}; + +/** Returns only tags which are badges. */ +const filterBadges = (tags: string[]): string[] => { + return tags.filter(tag => tag.startsWith('badge:')); +}; + +/** Get badges from an account. */ +const getBadges = (account: Account) => { + const tags = Array.from(account?.getIn(['pleroma', 'tags']) as Iterable || []); + return filterBadges(tags); +}; + +export { + tagToBadge, + badgeToTag, + filterBadges, + getTagDiff, + getBadges, +}; \ No newline at end of file From 2fc9b215a919960347d17adf18647071c2299d78 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 20:17:40 -0500 Subject: [PATCH 25/29] Display custom badges on profiles --- app/soapbox/components/badge.tsx | 37 ++++++++++--------- .../ui/components/profile_info_panel.tsx | 21 ++++++++--- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/app/soapbox/components/badge.tsx b/app/soapbox/components/badge.tsx index 13646bcdb..7ba2d5408 100644 --- a/app/soapbox/components/badge.tsx +++ b/app/soapbox/components/badge.tsx @@ -3,24 +3,27 @@ import React from 'react'; interface IBadge { title: React.ReactNode, - slug: 'patron' | 'donor' | 'admin' | 'moderator' | 'bot' | 'opaque', + slug: string, } - /** Badge to display on a user's profile. */ -const Badge: React.FC = ({ title, slug }) => ( - - {title} - -); +const Badge: React.FC = ({ title, slug }) => { + const fallback = !['patron', 'admin', 'moderator', 'opaque', 'donor', 'badge:donor'].includes(slug); + + return ( + + {title} + + ); +}; export default Badge; diff --git a/app/soapbox/features/ui/components/profile_info_panel.tsx b/app/soapbox/features/ui/components/profile_info_panel.tsx index 2c0a9163b..c6c6d084e 100644 --- a/app/soapbox/features/ui/components/profile_info_panel.tsx +++ b/app/soapbox/features/ui/components/profile_info_panel.tsx @@ -8,6 +8,8 @@ import { Icon, HStack, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification_badge'; import { useSoapboxConfig } from 'soapbox/hooks'; import { isLocal } from 'soapbox/utils/accounts'; +import { badgeToTag, getBadges as getAccountBadges } from 'soapbox/utils/badges'; +import { capitalize } from 'soapbox/utils/strings'; import ProfileFamiliarFollowers from './profile_familiar_followers'; import ProfileStats from './profile_stats'; @@ -52,7 +54,20 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => } }; + const getCustomBadges = (): React.ReactNode[] => { + const badges = getAccountBadges(account); + + return badges.map(badge => ( + + )); + }; + const getBadges = (): React.ReactNode[] => { + const custom = getCustomBadges(); const staffBadge = getStaffBadge(); const isPatron = account.getIn(['patron', 'is_patron']) === true; @@ -66,11 +81,7 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => badges.push(); } - if (account.donor) { - badges.push(); - } - - return badges; + return [...badges, ...custom]; }; const renderBirthday = (): React.ReactNode => { From 1f0b4d01b55d4beb1369659a18f6a53ce251a552 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 20:44:49 -0500 Subject: [PATCH 26/29] Remove account.donor as a concept, use 'badge:donor' custom tag instead --- app/soapbox/actions/admin.ts | 10 ---------- app/soapbox/components/badge.tsx | 4 ++-- app/soapbox/components/profile-hover-card.tsx | 4 ---- .../account-moderation-modal.tsx | 20 ------------------- app/soapbox/normalizers/account.ts | 7 ++++--- 5 files changed, 6 insertions(+), 39 deletions(-) diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts index ee4fae92c..50d389d2c 100644 --- a/app/soapbox/actions/admin.ts +++ b/app/soapbox/actions/admin.ts @@ -442,14 +442,6 @@ const unverifyUser = (accountId: string) => (dispatch: AppDispatch) => dispatch(untagUsers([accountId], ['verified'])); -const setDonor = (accountId: string) => - (dispatch: AppDispatch) => - dispatch(tagUsers([accountId], ['donor'])); - -const removeDonor = (accountId: string) => - (dispatch: AppDispatch) => - dispatch(untagUsers([accountId], ['donor'])); - const addPermission = (accountIds: string[], permissionGroup: string) => (dispatch: AppDispatch, getState: () => RootState) => { const nicknames = nicknamesFromIds(getState, accountIds); @@ -604,8 +596,6 @@ export { setBadges, verifyUser, unverifyUser, - setDonor, - removeDonor, addPermission, removePermission, promoteToAdmin, diff --git a/app/soapbox/components/badge.tsx b/app/soapbox/components/badge.tsx index 7ba2d5408..01b792bd0 100644 --- a/app/soapbox/components/badge.tsx +++ b/app/soapbox/components/badge.tsx @@ -7,14 +7,14 @@ interface IBadge { } /** Badge to display on a user's profile. */ const Badge: React.FC = ({ title, slug }) => { - const fallback = !['patron', 'admin', 'moderator', 'opaque', 'donor', 'badge:donor'].includes(slug); + const fallback = !['patron', 'admin', 'moderator', 'opaque', 'badge:donor'].includes(slug); return ( { badges.push(); } - if (account.donor) { - badges.push(); - } - return badges; }; diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx index d9d104b84..8f91e1d3c 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx @@ -4,8 +4,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { verifyUser, unverifyUser, - setDonor, - removeDonor, suggestUsers, unsuggestUsers, setBadges as saveBadges, @@ -80,17 +78,6 @@ const AccountModerationModal: React.FC = ({ onClose, ac .catch(() => {}); }; - const handleDonorChange: ChangeEventHandler = (e) => { - const { checked } = e.target; - - const message = checked ? messages.setDonorSuccess : messages.removeDonorSuccess; - const action = checked ? setDonor : removeDonor; - - dispatch(action(account.id)) - .then(() => dispatch(snackbar.success(intl.formatMessage(message, { acct: account.acct })))) - .catch(() => {}); - }; - const handleSuggestedChange: ChangeEventHandler = (e) => { const { checked } = e.target; @@ -147,13 +134,6 @@ const AccountModerationModal: React.FC = ({ onClose, ac /> - }> - - - {features.suggestionsV2 && ( }> ) => { }); }; -/** Get donor status from tags. */ +/** Upgrade legacy donor tag to a badge. */ const normalizeDonor = (account: ImmutableMap) => { - return account.set('donor', getTags(account).includes('donor')); + const tags = getTags(account); + const updated = tags.includes('donor') ? tags.push('badge:donor') : tags; + return account.setIn(['pleroma', 'tags'], updated); }; /** Normalize Fedibird/Truth Social/Pleroma location */ From 25f0ff9d865dc86d0cb115139ece884d87f0843f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 20:47:53 -0500 Subject: [PATCH 27/29] Allow removing legacy 'donor' tag --- app/soapbox/actions/admin.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts index 50d389d2c..82f244429 100644 --- a/app/soapbox/actions/admin.ts +++ b/app/soapbox/actions/admin.ts @@ -404,6 +404,12 @@ const tagUsers = (accountIds: string[], tags: string[]) => const untagUsers = (accountIds: string[], tags: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { const nicknames = nicknamesFromIds(getState, accountIds); + + // Legacy: allow removing legacy 'donor' tags. + if (tags.includes('badge:donor')) { + tags = [...tags, 'donor']; + } + dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags }); return api(getState) .delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } }) From 6f236dd1e615c8b0caf0f9fdebe49f57637baf2d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 21:04:21 -0500 Subject: [PATCH 28/29] Add utils/badges tests --- app/soapbox/utils/__tests__/badges.test.ts | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/soapbox/utils/__tests__/badges.test.ts diff --git a/app/soapbox/utils/__tests__/badges.test.ts b/app/soapbox/utils/__tests__/badges.test.ts new file mode 100644 index 000000000..1f3349fcc --- /dev/null +++ b/app/soapbox/utils/__tests__/badges.test.ts @@ -0,0 +1,43 @@ +import { normalizeAccount } from 'soapbox/normalizers'; + +import { + tagToBadge, + badgeToTag, + filterBadges, + getTagDiff, + getBadges, +} from '../badges'; + +import type { Account } from 'soapbox/types/entities'; + +test('tagToBadge', () => { + expect(tagToBadge('yolo')).toEqual('badge:yolo'); +}); + +test('badgeToTag', () => { + expect(badgeToTag('badge:yolo')).toEqual('yolo'); + expect(badgeToTag('badge:badge:')).toEqual('badge:'); +}); + +test('filterBadges', () => { + const tags = ['one', 'badge:two', 'badge:three', 'four']; + const badges = ['badge:two', 'badge:three']; + expect(filterBadges(tags)).toEqual(badges); +}); + +test('getTagDiff', () => { + const oldTags = ['one', 'two', 'three']; + const newTags = ['three', 'four', 'five']; + + const expected = { + added: ['four', 'five'], + removed: ['one', 'two'], + }; + + expect(getTagDiff(oldTags, newTags)).toEqual(expected); +}); + +test('getBadges', () => { + const account = normalizeAccount({ id: '1', pleroma: { tags: ['a', 'b', 'badge:c'] } }) as Account; + expect(getBadges(account)).toEqual(['badge:c']); +}); \ No newline at end of file From f472e0cca648e627c3192d44ed10b2a8ab9b01ab Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Sep 2022 21:59:16 -0500 Subject: [PATCH 29/29] setTags: call endpoints synchronously --- app/soapbox/actions/admin.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts index 82f244429..02af5e87a 100644 --- a/app/soapbox/actions/admin.ts +++ b/app/soapbox/actions/admin.ts @@ -422,13 +422,11 @@ const untagUsers = (accountIds: string[], tags: string[]) => /** Synchronizes user tags to the backend. */ const setTags = (accountId: string, oldTags: string[], newTags: string[]) => - (dispatch: AppDispatch) => { + async(dispatch: AppDispatch) => { const diff = getTagDiff(oldTags, newTags); - return Promise.all([ - dispatch(tagUsers([accountId], diff.added)), - dispatch(untagUsers([accountId], diff.removed)), - ]); + await dispatch(tagUsers([accountId], diff.added)); + await dispatch(untagUsers([accountId], diff.removed)); }; /** Synchronizes badges to the backend. */