From 3f9cc3cd04efb1274b649e0672afb1f05516ac70 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 24 Apr 2022 17:01:57 -0500 Subject: [PATCH] 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); - } -}