From 3f9cc3cd04efb1274b649e0672afb1f05516ac70 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 17:01:57 -0500 Subject: [PATCH 01/12] Restore Patron features, context to TSX --- app/soapbox/__fixtures__/patron-instance.json | 17 ++++ app/soapbox/__fixtures__/patron-user.json | 4 + app/soapbox/components/badge.tsx | 28 +++++-- app/soapbox/components/progress_bar.tsx | 4 +- .../features/ui/components/funding_panel.js | 78 ------------------ .../features/ui/components/funding_panel.tsx | 79 +++++++++++++++++++ app/soapbox/normalizers/account.ts | 3 +- app/soapbox/pages/home_page.js | 10 ++- app/soapbox/reducers/patron.js | 24 ------ app/soapbox/reducers/patron.ts | 50 ++++++++++++ app/soapbox/selectors/index.ts | 4 +- app/styles/application.scss | 1 - app/styles/components/badge.scss | 23 ------ app/styles/donations.scss | 13 --- 14 files changed, 184 insertions(+), 154 deletions(-) create mode 100644 app/soapbox/__fixtures__/patron-instance.json create mode 100644 app/soapbox/__fixtures__/patron-user.json delete mode 100644 app/soapbox/features/ui/components/funding_panel.js create mode 100644 app/soapbox/features/ui/components/funding_panel.tsx delete mode 100644 app/soapbox/reducers/patron.js create mode 100644 app/soapbox/reducers/patron.ts delete mode 100644 app/styles/components/badge.scss diff --git a/app/soapbox/__fixtures__/patron-instance.json b/app/soapbox/__fixtures__/patron-instance.json new file mode 100644 index 000000000..e8b82196c --- /dev/null +++ b/app/soapbox/__fixtures__/patron-instance.json @@ -0,0 +1,17 @@ +{ + "funding": { + "amount": 3500, + "patrons": 3, + "currency": "usd", + "interval": "monthly" + }, + "goals": [ + { + "amount": 20000, + "currency": "usd", + "interval": "monthly", + "text": "I'll be able to afford an avocado." + } + ], + "url": "https://patron.gleasonator.com" +} diff --git a/app/soapbox/__fixtures__/patron-user.json b/app/soapbox/__fixtures__/patron-user.json new file mode 100644 index 000000000..95d36674c --- /dev/null +++ b/app/soapbox/__fixtures__/patron-user.json @@ -0,0 +1,4 @@ +{ + "is_patron": true, + "url": "https://gleasonator.com/users/dave" +} diff --git a/app/soapbox/components/badge.tsx b/app/soapbox/components/badge.tsx index a816e7d38..b0626346c 100644 --- a/app/soapbox/components/badge.tsx +++ b/app/soapbox/components/badge.tsx @@ -1,13 +1,25 @@ -import PropTypes from 'prop-types'; +import classNames from 'classnames'; import React from 'react'; -const Badge = (props: any) => ( - {props.title} +interface IBadge { + title: string, + slug: 'patron' | 'admin' | 'moderator' | 'bot' | 'opaque', +} + +/** Badge to display on a user's profile. */ +const Badge: React.FC = ({ title, slug }) => ( + + {title} + ); -Badge.propTypes = { - title: PropTypes.string.isRequired, - slug: PropTypes.string.isRequired, -}; - export default Badge; diff --git a/app/soapbox/components/progress_bar.tsx b/app/soapbox/components/progress_bar.tsx index bcdc071b8..24e4e6790 100644 --- a/app/soapbox/components/progress_bar.tsx +++ b/app/soapbox/components/progress_bar.tsx @@ -5,8 +5,8 @@ interface IProgressBar { } const ProgressBar: React.FC = ({ progress }) => ( -
-
+
+
); diff --git a/app/soapbox/features/ui/components/funding_panel.js b/app/soapbox/features/ui/components/funding_panel.js deleted file mode 100644 index 45010360c..000000000 --- a/app/soapbox/features/ui/components/funding_panel.js +++ /dev/null @@ -1,78 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchPatronInstance } from 'soapbox/actions/patron'; -import Icon from 'soapbox/components/icon'; - -import ProgressBar from '../../../components/progress_bar'; - -const moneyFormat = amount => ( - new Intl - .NumberFormat('en-US', { - style: 'currency', - currency: 'usd', - notation: 'compact', - }) - .format(amount/100) -); - -class FundingPanel extends ImmutablePureComponent { - - componentDidMount() { - this.props.dispatch(fetchPatronInstance()); - } - - render() { - const { patron } = this.props; - if (patron.isEmpty()) return null; - - const amount = patron.getIn(['funding', 'amount']); - const goal = patron.getIn(['goals', '0', 'amount']); - const goal_text = patron.getIn(['goals', '0', 'text']); - const goal_reached = amount >= goal; - let ratio_text; - - if (goal_reached) { - ratio_text = <>{moneyFormat(goal)} per month— reached!; - } else { - ratio_text = <>{moneyFormat(amount)} out of {moneyFormat(goal)} per month; - } - - return ( -
-
- - - Funding Goal - -
-
-
- {ratio_text} -
- -
- {goal_text} -
- Donate -
-
- ); - } - -} - -const mapStateToProps = state => { - return { - patron: state.getIn(['patron', 'instance'], ImmutableMap()), - }; -}; - -export default injectIntl( - connect(mapStateToProps, null, null, { - forwardRef: true, - }, - )(FundingPanel)); diff --git a/app/soapbox/features/ui/components/funding_panel.tsx b/app/soapbox/features/ui/components/funding_panel.tsx new file mode 100644 index 000000000..7c104c2b7 --- /dev/null +++ b/app/soapbox/features/ui/components/funding_panel.tsx @@ -0,0 +1,79 @@ +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + +import { fetchPatronInstance } from 'soapbox/actions/patron'; +import { Widget, Button, Text } from 'soapbox/components/ui'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; + +import ProgressBar from '../../../components/progress_bar'; + +/** Open link in a new tab. */ +// https://stackoverflow.com/a/28374344/8811886 +const openInNewTab = (href: string): void => { + Object.assign(document.createElement('a'), { + target: '_blank', + href: href, + }).click(); +}; + +/** Formats integer to USD string. */ +const moneyFormat = (amount: number): string => ( + new Intl + .NumberFormat('en-US', { + style: 'currency', + currency: 'usd', + notation: 'compact', + }) + .format(amount/100) +); + +const FundingPanel: React.FC = () => { + const history = useHistory(); + const dispatch = useAppDispatch(); + const patron = useAppSelector(state => state.patron.instance); + + useEffect(() => { + dispatch(fetchPatronInstance()); + }, []); + + if (patron.funding.isEmpty() || patron.goals.isEmpty()) return null; + + const amount = patron.getIn(['funding', 'amount']) as number; + const goal = patron.getIn(['goals', '0', 'amount']) as number; + const goalText = patron.getIn(['goals', '0', 'text']) as string; + const goalReached = amount >= goal; + let ratioText; + + if (goalReached) { + ratioText = <>{moneyFormat(goal)} per month— reached!; + } else { + ratioText = <>{moneyFormat(amount)} out of {moneyFormat(goal)} per month; + } + + const handleDonateClick = () => { + openInNewTab(patron.url); + }; + + return ( + } + onActionClick={handleDonateClick} + > +
+ {ratioText} +
+ +
+ {goalText} +
+
+ +
+
+ ); +}; + +export default FundingPanel; diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index 2ca866c18..eac930ec9 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -16,6 +16,7 @@ import { normalizeEmoji } from 'soapbox/normalizers/emoji'; import { unescapeHTML } from 'soapbox/utils/html'; import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers'; +import type { PatronAccount } from 'soapbox/reducers/patron'; import type { Emoji, Field, EmbeddedEntity } from 'soapbox/types/entities'; // https://docs.joinmastodon.org/entities/account/ @@ -57,7 +58,7 @@ export const AccountRecord = ImmutableRecord({ moderator: false, note_emojified: '', note_plain: '', - patron: ImmutableMap(), + patron: null as PatronAccount | null, relationship: ImmutableList>(), should_refetch: false, staff: false, diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js index fcff5b1e3..68b41961a 100644 --- a/app/soapbox/pages/home_page.js +++ b/app/soapbox/pages/home_page.js @@ -11,6 +11,7 @@ import { TrendsPanel, SignUpPanel, PromoPanel, + FundingPanel, CryptoDonatePanel, BirthdayPanel, } from 'soapbox/features/ui/util/async-components'; @@ -33,7 +34,7 @@ const mapStateToProps = state => { return { me, account: state.getIn(['accounts', me]), - showFundingPanel: hasPatron, + hasPatron, hasCrypto, cryptoLimit, features, @@ -49,7 +50,7 @@ class HomePage extends ImmutablePureComponent { } render() { - const { me, children, account, features, hasCrypto, cryptoLimit } = this.props; + const { me, children, account, features, hasPatron, hasCrypto, cryptoLimit } = this.props; const acct = account ? account.get('acct') : ''; @@ -90,6 +91,11 @@ class HomePage extends ImmutablePureComponent { {Component => } )} + {hasPatron && ( + + {Component => } + + )} {hasCrypto && cryptoLimit > 0 && ( {Component => } diff --git a/app/soapbox/reducers/patron.js b/app/soapbox/reducers/patron.js deleted file mode 100644 index 66a8940d8..000000000 --- a/app/soapbox/reducers/patron.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { - PATRON_INSTANCE_FETCH_SUCCESS, - PATRON_ACCOUNT_FETCH_SUCCESS, -} from '../actions/patron'; - -const initialState = ImmutableMap(); - -const normalizePatronAccount = (state, account) => { - const normalized = fromJS(account).deleteAll(['url']); - return state.setIn(['accounts', account.url], normalized); -}; - -export default function patron(state = initialState, action) { - switch(action.type) { - case PATRON_INSTANCE_FETCH_SUCCESS: - return state.set('instance', fromJS(action.instance)); - case PATRON_ACCOUNT_FETCH_SUCCESS: - return normalizePatronAccount(state, action.account); - default: - return state; - } -} diff --git a/app/soapbox/reducers/patron.ts b/app/soapbox/reducers/patron.ts new file mode 100644 index 000000000..edf4c1c43 --- /dev/null +++ b/app/soapbox/reducers/patron.ts @@ -0,0 +1,50 @@ +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, + fromJS, +} from 'immutable'; + +import { + PATRON_INSTANCE_FETCH_SUCCESS, + PATRON_ACCOUNT_FETCH_SUCCESS, +} from '../actions/patron'; + +import type { AnyAction } from 'redux'; + +const PatronAccountRecord = ImmutableRecord({ + is_patron: false, + url: '', +}); + +const PatronInstanceRecord = ImmutableRecord({ + funding: ImmutableMap(), + goals: ImmutableList(), + url: '', +}); + +const ReducerRecord = ImmutableRecord({ + instance: PatronInstanceRecord() as PatronInstance, + accounts: ImmutableMap(), +}); + +type State = ReturnType; + +export type PatronAccount = ReturnType; +export type PatronInstance = ReturnType; + +const normalizePatronAccount = (state: State, account: Record) => { + const normalized = PatronAccountRecord(account); + return state.setIn(['accounts', normalized.url], normalized); +}; + +export default function patron(state = ReducerRecord(), action: AnyAction) { + switch(action.type) { + case PATRON_INSTANCE_FETCH_SUCCESS: + return state.set('instance', PatronInstanceRecord(ImmutableMap(fromJS(action.instance)))); + case PATRON_ACCOUNT_FETCH_SUCCESS: + return normalizePatronAccount(state, action.account); + default: + return state; + } +} diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 40f8105e3..abc8f1811 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -26,7 +26,7 @@ const getAccountMeta = (state: RootState, id: string) => state.accounts_ const getAccountAdminData = (state: RootState, id: string) => state.admin.users.get(id); const getAccountPatron = (state: RootState, id: string) => { const url = state.accounts.get(id)?.url; - return state.patron.getIn(['accounts', url]); + return url ? state.patron.accounts.get(url) : null; }; export const makeGetAccount = () => { @@ -47,7 +47,7 @@ export const makeGetAccount = () => { map.set('pleroma', meta.get('pleroma', ImmutableMap()).merge(base.get('pleroma', ImmutableMap()))); // Lol, thanks Pleroma map.set('relationship', relationship); map.set('moved', moved || null); - map.set('patron', patron); + map.set('patron', patron || null); map.setIn(['pleroma', 'admin'], admin); }); }); diff --git a/app/styles/application.scss b/app/styles/application.scss index 64478a2f5..8c5a05553 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -63,7 +63,6 @@ @import 'components/getting-started'; @import 'components/promo-panel'; @import 'components/still-image'; -@import 'components/badge'; @import 'components/theme-toggle'; @import 'components/trends'; @import 'components/wtf-panel'; diff --git a/app/styles/components/badge.scss b/app/styles/components/badge.scss deleted file mode 100644 index 1710c6e47..000000000 --- a/app/styles/components/badge.scss +++ /dev/null @@ -1,23 +0,0 @@ -.badge { - @apply inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-primary-600 text-white; - - &--patron { - @apply bg-primary-600 text-white; - } - - &--admin { - @apply bg-black; - } - - &--moderator { - @apply bg-cyan-600 text-white; - } - - &--bot { - @apply bg-gray-100 text-gray-800; - } - - &--opaque { - @apply bg-white bg-opacity-75 text-gray-900; - } -} diff --git a/app/styles/donations.scss b/app/styles/donations.scss index c66ce0b00..6709189cc 100644 --- a/app/styles/donations.scss +++ b/app/styles/donations.scss @@ -198,16 +198,3 @@ body.admin { padding: 15px; } } - -.progress-bar { - height: 8px; - width: 100%; - border-radius: 4px; - background: var(--background-color); - overflow: hidden; - - &__progress { - height: 100%; - background: var(--brand-color); - } -} From 73f086c680a1d64e5cb0a2f080196b90d59e19db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 17:27:08 -0500 Subject: [PATCH 02/12] Add "Donor" badge --- app/soapbox/actions/admin.js | 12 ++++++++ app/soapbox/components/badge.tsx | 3 +- app/soapbox/components/profile_hover_card.js | 4 +++ .../features/account/components/header.js | 22 ++++++++++++-- .../account_timeline/components/header.js | 10 +++++++ .../containers/header_container.js | 21 ++++++++++++++ .../ui/components/profile_info_panel.js | 4 +++ app/soapbox/normalizers/account.ts | 29 ++++++++++++------- app/soapbox/reducers/accounts.ts | 22 ++++++++++++++ 9 files changed, 112 insertions(+), 15 deletions(-) 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; + } + }); }); }); }; From 187af8b7d24ecbf1b08a3b3618c61dae0b6db94c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 17:53:03 -0500 Subject: [PATCH 03/12] Improve dropdown menu styles --- app/soapbox/components/dropdown_menu.tsx | 8 ++++---- app/soapbox/components/ui/icon/svg-icon.tsx | 2 ++ app/soapbox/features/account/components/header.js | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/soapbox/components/dropdown_menu.tsx b/app/soapbox/components/dropdown_menu.tsx index e28ea0f9e..4bd747d0a 100644 --- a/app/soapbox/components/dropdown_menu.tsx +++ b/app/soapbox/components/dropdown_menu.tsx @@ -6,8 +6,8 @@ import { spring } from 'react-motion'; import Overlay from 'react-overlays/lib/Overlay'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import Icon from 'soapbox/components/icon'; import { IconButton } from 'soapbox/components/ui'; +import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; import Motion from 'soapbox/features/ui/util/optional_motion'; import type { Status } from 'soapbox/types/entities'; @@ -177,7 +177,7 @@ class DropdownMenu extends React.PureComponent +
  • - {icon && } - {text} + {icon && } + {text}
  • ); diff --git a/app/soapbox/components/ui/icon/svg-icon.tsx b/app/soapbox/components/ui/icon/svg-icon.tsx index 84604150d..c84e40203 100644 --- a/app/soapbox/components/ui/icon/svg-icon.tsx +++ b/app/soapbox/components/ui/icon/svg-icon.tsx @@ -29,6 +29,8 @@ const SvgIcon: React.FC = ({ src, alt, size = 24, className }): JSX.El height={size} loader={loader} data-testid='svg-icon' + // The cache causes bugs, and the ServiceWorker already caches icons. + cacheRequests={false} > {/* If the fetch fails, fall back to displaying the loader */} {loader} diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index c1d807546..1db5b1dd3 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -630,7 +630,7 @@ class Header extends ImmutablePureComponent { return (
    - +
    {menuItem.text}
    From e648162f662e916dc71ef8de8c3f256a95c0abf8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 17:59:12 -0500 Subject: [PATCH 04/12] Modal: fix pager style --- app/styles/components/modal.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 0b2e8ddba..50e1a506f 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -159,7 +159,7 @@ } .media-modal__button--active { - background-color: var(--highlight-text-color); + @apply bg-accent-500; } .media-modal__close { From b8eff3e46b0542600583050af5ddb45f19b83927 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 18:07:20 -0500 Subject: [PATCH 05/12] VideoModal: convert to TSX --- .../features/ui/components/video_modal.js | 53 ------------------- .../features/ui/components/video_modal.tsx | 51 ++++++++++++++++++ 2 files changed, 51 insertions(+), 53 deletions(-) delete mode 100644 app/soapbox/features/ui/components/video_modal.js create mode 100644 app/soapbox/features/ui/components/video_modal.tsx diff --git a/app/soapbox/features/ui/components/video_modal.js b/app/soapbox/features/ui/components/video_modal.js deleted file mode 100644 index 066aa9d79..000000000 --- a/app/soapbox/features/ui/components/video_modal.js +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import { withRouter } from 'react-router-dom'; - -import Video from 'soapbox/features/video'; - -export default @withRouter -class VideoModal extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - status: ImmutablePropTypes.record, - account: ImmutablePropTypes.record, - time: PropTypes.number, - onClose: PropTypes.func.isRequired, - history: PropTypes.object, - }; - - handleStatusClick = e => { - const { status, account } = this.props; - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/@${account.get('acct')}/posts/${status.get('id')}`); - } - } - - render() { - const { media, status, account, time, onClose } = this.props; - - const link = status && account && ; - - return ( -
    -
    -
    -
    - ); - } - -} diff --git a/app/soapbox/features/ui/components/video_modal.tsx b/app/soapbox/features/ui/components/video_modal.tsx new file mode 100644 index 000000000..c703e51db --- /dev/null +++ b/app/soapbox/features/ui/components/video_modal.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + +import Video from 'soapbox/features/video'; + +import type { Status, Account, Attachment } from 'soapbox/types/entities'; + +interface IVideoModal { + media: Attachment, + status: Status, + account: Account, + time: number, + onClose: () => void, +} + +const VideoModal: React.FC = ({ status, account, media, time, onClose }) => { + const history = useHistory(); + + const handleStatusClick: React.MouseEventHandler = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + history.push(`/@${account.acct}/posts/${status.id}`); + } + }; + + const link = status && account && ( + + + + ); + + return ( +
    +
    +
    +
    + ); +}; + +export default VideoModal; From 18c4210cec90930913926e44d2a00d1b774af126 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 18:24:57 -0500 Subject: [PATCH 06/12] Fix VideoModal, remove Video close button --- .../features/ui/components/video_modal.tsx | 24 +++++++++---------- app/soapbox/features/video/index.js | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/app/soapbox/features/ui/components/video_modal.tsx b/app/soapbox/features/ui/components/video_modal.tsx index c703e51db..71a9d4e81 100644 --- a/app/soapbox/features/ui/components/video_modal.tsx +++ b/app/soapbox/features/ui/components/video_modal.tsx @@ -31,19 +31,17 @@ const VideoModal: React.FC = ({ status, account, media, time, onClo ); return ( -
    -
    -
    +
    +
    ); }; diff --git a/app/soapbox/features/video/index.js b/app/soapbox/features/video/index.js index 72d18be0e..43a7f9f17 100644 --- a/app/soapbox/features/video/index.js +++ b/app/soapbox/features/video/index.js @@ -614,8 +614,8 @@ class Video extends React.PureComponent {
    {(sensitive && !onCloseVideo) && } {(!fullscreen && onOpenVideo) && } - {onCloseVideo && } - + {/* onCloseVideo && */} +
    From 8586c8ded561ff33932851566b506a36d75e0f94 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 18:35:23 -0500 Subject: [PATCH 07/12] LinkFooter: add breathing room --- app/soapbox/components/ui/layout/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/components/ui/layout/layout.tsx b/app/soapbox/components/ui/layout/layout.tsx index f5e044a31..0959fe060 100644 --- a/app/soapbox/components/ui/layout/layout.tsx +++ b/app/soapbox/components/ui/layout/layout.tsx @@ -31,7 +31,7 @@ const Main: React.FC> = ({ children, classN const Aside: React.FC = ({ children }) => ( From eaf15fd77caa1468099cfe42d9f3f7359293dc0c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 18:38:12 -0500 Subject: [PATCH 08/12] LinkFooter: improve style of repo link --- app/soapbox/features/ui/components/link_footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/ui/components/link_footer.tsx b/app/soapbox/features/ui/components/link_footer.tsx index da0fc48d6..4c81c5539 100644 --- a/app/soapbox/features/ui/components/link_footer.tsx +++ b/app/soapbox/features/ui/components/link_footer.tsx @@ -75,7 +75,7 @@ const LinkFooter: React.FC = (): JSX.Element => { defaultMessage='{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).' values={{ code_name: sourceCode.displayName, - code_link: {sourceCode.repository}, + code_link: {sourceCode.repository}, code_version: sourceCode.version, }} /> From 000615ced442d01dc09ee0516560ee3eb7581e91 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 18:54:46 -0500 Subject: [PATCH 09/12] CryptoAddress: improve icon size --- .../components/crypto_address.tsx | 4 +-- app/soapbox/features/crypto_donate/index.tsx | 30 +++++++++---------- .../features/ui/components/accordion.tsx | 5 ++-- app/styles/components/accordion.scss | 3 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/soapbox/features/crypto_donate/components/crypto_address.tsx b/app/soapbox/features/crypto_donate/components/crypto_address.tsx index 716e4eee5..3fb28f6e4 100644 --- a/app/soapbox/features/crypto_donate/components/crypto_address.tsx +++ b/app/soapbox/features/crypto_donate/components/crypto_address.tsx @@ -42,12 +42,12 @@ const CryptoAddress: React.FC = (props): JSX.Element => { - + {explorerUrl && ( - + )} diff --git a/app/soapbox/features/crypto_donate/index.tsx b/app/soapbox/features/crypto_donate/index.tsx index 86de895c9..e1e38d7fc 100644 --- a/app/soapbox/features/crypto_donate/index.tsx +++ b/app/soapbox/features/crypto_donate/index.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { Column } from 'soapbox/components/ui'; +import { Column, Stack } from 'soapbox/components/ui'; import Accordion from 'soapbox/features/ui/components/accordion'; import { useAppSelector } from 'soapbox/hooks'; @@ -18,23 +18,21 @@ const CryptoDonate: React.FC = (): JSX.Element => { return ( -
    -
    - } - expanded={explanationBoxExpanded} - onToggle={toggleExplanationBox} - > - - + + } + expanded={explanationBoxExpanded} + onToggle={toggleExplanationBox} + > + + -
    -
    +
    ); }; diff --git a/app/soapbox/features/ui/components/accordion.tsx b/app/soapbox/features/ui/components/accordion.tsx index b3f04262b..8fe4415a8 100644 --- a/app/soapbox/features/ui/components/accordion.tsx +++ b/app/soapbox/features/ui/components/accordion.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { Text } from 'soapbox/components/ui'; import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; import type { Menu } from 'soapbox/components/dropdown_menu'; @@ -40,10 +41,10 @@ const Accordion: React.FC = ({ headline, children, menu, expanded = onClick={handleToggle} title={intl.formatMessage(expanded ? messages.collapse : messages.expand)} > - {headline} + {headline}
    - {children} + {children}
    ); diff --git a/app/styles/components/accordion.scss b/app/styles/components/accordion.scss index e52cb10ee..00a14196e 100644 --- a/app/styles/components/accordion.scss +++ b/app/styles/components/accordion.scss @@ -3,7 +3,7 @@ } .accordion { - @apply text-black dark:text-white bg-gray-100 dark:bg-slate-800; + @apply text-black dark:text-white bg-gray-100 dark:bg-slate-900; padding: 15px 20px; font-size: 14px; border-radius: 8px; @@ -50,6 +50,7 @@ margin-bottom: 10px !important; &::after { + @apply text-black dark:text-white; content: ''; } } From d9903807ed083a8936477286c55800c633387af3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 18:56:57 -0500 Subject: [PATCH 10/12] FundingPanel: remove unused history variable --- app/soapbox/features/ui/components/funding_panel.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/features/ui/components/funding_panel.tsx b/app/soapbox/features/ui/components/funding_panel.tsx index 7c104c2b7..fa2ad8911 100644 --- a/app/soapbox/features/ui/components/funding_panel.tsx +++ b/app/soapbox/features/ui/components/funding_panel.tsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { fetchPatronInstance } from 'soapbox/actions/patron'; import { Widget, Button, Text } from 'soapbox/components/ui'; @@ -29,7 +28,6 @@ const moneyFormat = (amount: number): string => ( ); const FundingPanel: React.FC = () => { - const history = useHistory(); const dispatch = useAppDispatch(); const patron = useAppSelector(state => state.patron.instance); From e64d93abcb83f4d85b6613b45b76838b856c9073 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 19:02:25 -0500 Subject: [PATCH 11/12] Fix Patron tests --- app/soapbox/reducers/__tests__/patron-test.js | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/soapbox/reducers/__tests__/patron-test.js b/app/soapbox/reducers/__tests__/patron-test.js index b83d2c025..01736704b 100644 --- a/app/soapbox/reducers/__tests__/patron-test.js +++ b/app/soapbox/reducers/__tests__/patron-test.js @@ -1,11 +1,13 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { Record as ImmutableRecord } from 'immutable'; import { PATRON_ACCOUNT_FETCH_SUCCESS } from '../../actions/patron'; import reducer from '../patron'; describe('patron reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap()); + const result = reducer(undefined, {}); + expect(ImmutableRecord.isRecord(result)).toBe(true); + expect(result.instance.url).toBe(''); }); describe('PATRON_ACCOUNT_FETCH_SUCCESS', () => { @@ -17,14 +19,15 @@ describe('patron reducer', () => { is_patron: true, }, }; - const state = ImmutableMap(); - expect(reducer(state, action)).toEqual(fromJS({ - accounts: { - 'https://gleasonator.com/users/alex': { - is_patron: true, - }, + + const result = reducer(undefined, action); + + expect(result.accounts.toJS()).toEqual({ + 'https://gleasonator.com/users/alex': { + is_patron: true, + url: 'https://gleasonator.com/users/alex', }, - })); + }); }); }); }); From 3bd272622a1075ca845f487c91908090e72b01e7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 19:07:10 -0500 Subject: [PATCH 12/12] Nvm, definitely do cache icon requests --- app/soapbox/components/ui/icon/svg-icon.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/components/ui/icon/svg-icon.tsx b/app/soapbox/components/ui/icon/svg-icon.tsx index c84e40203..84604150d 100644 --- a/app/soapbox/components/ui/icon/svg-icon.tsx +++ b/app/soapbox/components/ui/icon/svg-icon.tsx @@ -29,8 +29,6 @@ const SvgIcon: React.FC = ({ src, alt, size = 24, className }): JSX.El height={size} loader={loader} data-testid='svg-icon' - // The cache causes bugs, and the ServiceWorker already caches icons. - cacheRequests={false} > {/* If the fetch fails, fall back to displaying the loader */} {loader}