diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 414cb07d6..3704e114e 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -274,6 +274,18 @@ export function unverifyUser(accountId) { }; } +export function setDonor(accountId) { + return (dispatch, getState) => { + return dispatch(tagUsers([accountId], ['donor'])); + }; +} + +export function removeDonor(accountId) { + return (dispatch, getState) => { + return dispatch(untagUsers([accountId], ['donor'])); + }; +} + export function addPermission(accountIds, permissionGroup) { return (dispatch, getState) => { const nicknames = nicknamesFromIds(getState, accountIds); diff --git a/app/soapbox/components/badge.tsx b/app/soapbox/components/badge.tsx index b0626346c..7ca11c863 100644 --- a/app/soapbox/components/badge.tsx +++ b/app/soapbox/components/badge.tsx @@ -3,7 +3,7 @@ import React from 'react'; interface IBadge { title: string, - slug: 'patron' | 'admin' | 'moderator' | 'bot' | 'opaque', + slug: 'patron' | 'donor' | 'admin' | 'moderator' | 'bot' | 'opaque', } /** Badge to display on a user's profile. */ @@ -12,6 +12,7 @@ const Badge: React.FC = ({ title, slug }) => ( data-testid='badge' className={classNames('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white', { 'bg-fuchsia-700': slug === 'patron', + 'bg-yellow-500': slug === 'donor', 'bg-black': slug === 'admin', 'bg-cyan-600': slug === 'moderator', 'bg-gray-100 text-gray-800': slug === 'bot', diff --git a/app/soapbox/components/profile_hover_card.js b/app/soapbox/components/profile_hover_card.js index c435261a4..b974623db 100644 --- a/app/soapbox/components/profile_hover_card.js +++ b/app/soapbox/components/profile_hover_card.js @@ -36,6 +36,10 @@ const getBadges = (account) => { badges.push(); } + if (account.donor) { + badges.push(); + } + return badges; }; diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index bf60de896..c1d807546 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -54,6 +54,8 @@ 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}' }, + setDonor: { id: 'admin.users.actions.set_donor', defaultMessage: 'Set @{name} as a donor' }, + removeDonor: { id: 'admin.users.actions.remove_donor', defaultMessage: 'Remove @{name} as a donor' }, 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' }, @@ -386,20 +388,34 @@ class Header extends ImmutablePureComponent { } } - if (account.get('verified')) { + if (account.verified) { menu.push({ - text: intl.formatMessage(messages.unverifyUser, { name: account.get('username') }), + text: intl.formatMessage(messages.unverifyUser, { name: account.username }), action: this.props.onUnverifyUser, icon: require('@tabler/icons/icons/check.svg'), }); } else { menu.push({ - text: intl.formatMessage(messages.verifyUser, { name: account.get('username') }), + text: intl.formatMessage(messages.verifyUser, { name: account.username }), action: this.props.onVerifyUser, icon: require('@tabler/icons/icons/check.svg'), }); } + if (account.donor) { + menu.push({ + text: intl.formatMessage(messages.removeDonor, { name: account.username }), + action: this.props.onRemoveDonor, + icon: require('@tabler/icons/icons/coin.svg'), + }); + } else { + menu.push({ + text: intl.formatMessage(messages.setDonor, { name: account.username }), + action: this.props.onSetDonor, + icon: require('@tabler/icons/icons/coin.svg'), + }); + } + if (features.suggestionsV2 && meAccount.admin) { if (account.getIn(['pleroma', 'is_suggested'])) { menu.push({ diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index f827e9314..740225878 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -110,6 +110,14 @@ class Header extends ImmutablePureComponent { this.props.onUnverifyUser(this.props.account); } + handleSetDonor = () => { + this.props.onSetDonor(this.props.account); + } + + handleRemoveDonor = () => { + this.props.onRemoveDonor(this.props.account); + } + handlePromoteToAdmin = () => { this.props.onPromoteToAdmin(this.props.account); } @@ -163,6 +171,8 @@ class Header extends ImmutablePureComponent { onDeleteUser={this.handleDeleteUser} onVerifyUser={this.handleVerifyUser} onUnverifyUser={this.handleUnverifyUser} + onSetDonor={this.handleSetDonor} + onRemoveDonor={this.handleRemoveDonor} onPromoteToAdmin={this.handlePromoteToAdmin} onPromoteToModerator={this.handlePromoteToModerator} onDemoteToUser={this.handleDemoteToUser} diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js index 6b4ff5e6a..4fbfa6d01 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -18,6 +18,8 @@ import { import { verifyUser, unverifyUser, + setDonor, + removeDonor, promoteToAdmin, promoteToModerator, demoteToUser, @@ -47,6 +49,8 @@ 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' }, + setDonor: { id: 'admin.users.set_donor_message', defaultMessage: '@{acct} was set as a donor' }, + removeDonor: { id: 'admin.users.remove_donor_message', defaultMessage: '@{acct} was removed as a donor' }, 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' }, @@ -206,6 +210,23 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ .catch(() => {}); }, + onSetDonor(account) { + const message = intl.formatMessage(messages.setDonor, { acct: account.get('acct') }); + + dispatch(setDonor(account.get('id'))) + .then(() => dispatch(snackbar.success(message))) + .catch(() => {}); + }, + + onRemoveDonor(account) { + const message = intl.formatMessage(messages.removeDonor, { acct: account.get('acct') }); + + dispatch(removeDonor(account.get('id'))) + .then(() => dispatch(snackbar.success(message))) + .catch(() => {}); + }, + + onPromoteToAdmin(account) { const message = intl.formatMessage(messages.promotedToAdmin, { acct: account.get('acct') }); diff --git a/app/soapbox/features/ui/components/profile_info_panel.js b/app/soapbox/features/ui/components/profile_info_panel.js index e4547e270..9b1f83c4e 100644 --- a/app/soapbox/features/ui/components/profile_info_panel.js +++ b/app/soapbox/features/ui/components/profile_info_panel.js @@ -72,6 +72,10 @@ class ProfileInfoPanel extends ImmutablePureComponent { badges.push(); } + if (account.donor) { + badges.push(); + } + return badges; } diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index eac930ec9..cdb80dfd4 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -55,6 +55,7 @@ export const AccountRecord = ImmutableRecord({ admin: false, display_name_html: '', domain: '', + donor: false, moderator: false, note_emojified: '', note_plain: '', @@ -92,7 +93,7 @@ const normalizePleromaLegacyFields = (account: ImmutableMap) => { }); }; -// Add avatar, if missing +/** Add avatar, if missing */ const normalizeAvatar = (account: ImmutableMap) => { const avatar = account.get('avatar'); const avatarStatic = account.get('avatar_static'); @@ -104,7 +105,7 @@ const normalizeAvatar = (account: ImmutableMap) => { }); }; -// Add header, if missing +/** Add header, if missing */ const normalizeHeader = (account: ImmutableMap) => { const header = account.get('header'); const headerStatic = account.get('header_static'); @@ -116,18 +117,18 @@ const normalizeHeader = (account: ImmutableMap) => { }); }; -// Normalize custom fields +/** Normalize custom fields */ const normalizeFields = (account: ImmutableMap) => { return account.update('fields', ImmutableList(), fields => fields.map(FieldRecord)); }; -// Normalize emojis +/** Normalize emojis */ const normalizeEmojis = (entity: ImmutableMap) => { const emojis = entity.get('emojis', ImmutableList()).map(normalizeEmoji); return entity.set('emojis', emojis); }; -// Normalize Pleroma/Fedibird birthday +/** Normalize Pleroma/Fedibird birthday */ const normalizeBirthday = (account: ImmutableMap) => { const birthday = [ account.getIn(['pleroma', 'birthday']), @@ -137,13 +138,13 @@ const normalizeBirthday = (account: ImmutableMap) => { return account.set('birthday', birthday); }; -// Get Pleroma tags +/** Get Pleroma tags */ const getTags = (account: ImmutableMap): ImmutableList => { const tags = account.getIn(['pleroma', 'tags']); return ImmutableList(ImmutableList.isList(tags) ? tags : []); }; -// Normalize Truth Social/Pleroma verified +/** Normalize Truth Social/Pleroma verified */ const normalizeVerified = (account: ImmutableMap) => { return account.update('verified', verified => { return [ @@ -153,7 +154,12 @@ const normalizeVerified = (account: ImmutableMap) => { }); }; -// Normalize Fedibird/Truth Social/Pleroma location +/** Get donor status from tags. */ +const normalizeDonor = (account: ImmutableMap) => { + return account.set('donor', getTags(account).includes('donor')); +}; + +/** Normalize Fedibird/Truth Social/Pleroma location */ const normalizeLocation = (account: ImmutableMap) => { return account.update('location', location => { return [ @@ -164,20 +170,20 @@ const normalizeLocation = (account: ImmutableMap) => { }); }; -// Set username from acct, if applicable +/** Set username from acct, if applicable */ const fixUsername = (account: ImmutableMap) => { const acct = account.get('acct') || ''; const username = account.get('username') || ''; return account.set('username', username || acct.split('@')[0]); }; -// Set display name from username, if applicable +/** Set display name from username, if applicable */ const fixDisplayName = (account: ImmutableMap) => { const displayName = account.get('display_name') || ''; return account.set('display_name', displayName.trim().length === 0 ? account.get('username') : displayName); }; -// Emojification, etc +/** Emojification, etc */ const addInternalFields = (account: ImmutableMap) => { const emojiMap = makeEmojiMap(account.get('emojis')); @@ -258,6 +264,7 @@ export const normalizeAccount = (account: Record) => { normalizeHeader(account); normalizeFields(account); normalizeVerified(account); + normalizeDonor(account); normalizeBirthday(account); normalizeLocation(account); normalizeFqn(account); diff --git a/app/soapbox/reducers/accounts.ts b/app/soapbox/reducers/accounts.ts index ddc3a813b..63f479adb 100644 --- a/app/soapbox/reducers/accounts.ts +++ b/app/soapbox/reducers/accounts.ts @@ -91,6 +91,17 @@ const addTags = ( state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => ImmutableOrderedSet(fromJS(v)).union(tags).toList(), ); + + tags.forEach(tag => { + switch(tag) { + case 'verified': + state.setIn([id, 'verified'], true); + break; + case 'donor': + state.setIn([id, 'donor'], true); + break; + } + }); }); }); }; @@ -105,6 +116,17 @@ const removeTags = ( state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => ImmutableOrderedSet(fromJS(v)).subtract(tags).toList(), ); + + tags.forEach(tag => { + switch(tag) { + case 'verified': + state.setIn([id, 'verified'], false); + break; + case 'donor': + state.setIn([id, 'donor'], false); + break; + } + }); }); }); };