From acfca37dec6c2dce065ccbad9e061616842789e4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Jun 2021 12:56:27 -0500 Subject: [PATCH 1/5] CryptoDonate: add CryptoDonateWidget to homepage --- app/soapbox/actions/soapbox.js | 3 + .../components/crypto_donate_panel.js | 65 +++++++++++++++++++ .../crypto_donate/components/site_wallet.js | 9 ++- app/soapbox/pages/home_page.js | 9 ++- app/styles/components/crypto-donate.scss | 32 +++++++++ app/styles/components/wtf-panel.scss | 19 ++++++ 6 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 app/soapbox/features/crypto_donate/components/crypto_donate_panel.js diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index e732b1f40..7e03a1e52 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -43,6 +43,9 @@ export const defaultConfig = ImmutableMap({ allowedEmoji: allowedEmoji, verifiedCanEditName: false, displayFqn: true, + cryptoDonatePanel: ImmutableMap({ + limit: 3, + }), }); export function getSoapboxConfig(state) { diff --git a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js new file mode 100644 index 000000000..b63acdad1 --- /dev/null +++ b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import SiteWallet from './site_wallet'; +import { List as ImmutableList } from 'immutable'; +import classNames from 'classnames'; + +const mapStateToProps = state => { + const addresses = state.getIn(['soapbox', 'crypto_addresses'], ImmutableList()); + return { + total: addresses.size, + siteTitle: state.getIn(['instance', 'title']), + }; +}; + +export default @connect(mapStateToProps) +class CryptoDonatePanel extends ImmutablePureComponent { + + static propTypes = { + limit: PropTypes.number, + total: PropTypes.number, + } + + static defaultProps = { + limit: 3, + } + + render() { + const { limit, total, siteTitle } = this.props; + const more = total - limit; + const hasMore = more > 0; + + return ( +
+
+ + + + +
+
+
+ +
+ +
+ {hasMore && + + } +
+ ); + } + +}; diff --git a/app/soapbox/features/crypto_donate/components/site_wallet.js b/app/soapbox/features/crypto_donate/components/site_wallet.js index 3e5d30bc5..5e5e3fa69 100644 --- a/app/soapbox/features/crypto_donate/components/site_wallet.js +++ b/app/soapbox/features/crypto_donate/components/site_wallet.js @@ -1,14 +1,18 @@ import React from 'react'; import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import CryptoAddress from './crypto_address'; -const mapStateToProps = state => { +const mapStateToProps = (state, ownProps) => { // Address example: // {"ticker": "btc", "address": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n", "note": "This is our main address"} + const addresses = state.getIn(['soapbox', 'crypto_addresses']); + const { limit } = ownProps; + return { - coinList: state.getIn(['soapbox', 'crypto_addresses']), + coinList: typeof limit === 'number' ? addresses.take(limit) : addresses, }; }; @@ -17,6 +21,7 @@ class CoinList extends ImmutablePureComponent { static propTypes = { coinList: ImmutablePropTypes.list, + limit: PropTypes.number, } render() { diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js index 50233bb70..435752196 100644 --- a/app/soapbox/pages/home_page.js +++ b/app/soapbox/pages/home_page.js @@ -8,6 +8,7 @@ import FeaturesPanel from '../features/ui/components/features_panel'; import PromoPanel from '../features/ui/components/promo_panel'; import UserPanel from '../features/ui/components/user_panel'; import FundingPanel from '../features/ui/components/funding_panel'; +import CryptoDonatePanel from 'soapbox/features/crypto_donate/components/crypto_donate_panel'; import ComposeFormContainer from '../features/compose/containers/compose_form_container'; import Avatar from '../components/avatar'; import { getFeatures } from 'soapbox/utils/features'; @@ -16,10 +17,13 @@ import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const mapStateToProps = state => { const me = state.get('me'); + const soapbox = getSoapboxConfig(state); return { me, account: state.getIn(['accounts', me]), - hasPatron: getSoapboxConfig(state).getIn(['extensions', 'patron', 'enabled']), + hasPatron: soapbox.getIn(['extensions', 'patron', 'enabled']), + hasCrypto: typeof soapbox.getIn(['crypto_addresses', 0, 'ticker']) === 'string', + cryptoLimit: soapbox.getIn(['cryptoDonatePanel', 'limit']), features: getFeatures(state.get('instance')), }; }; @@ -33,7 +37,7 @@ class HomePage extends ImmutablePureComponent { } render() { - const { me, children, account, hasPatron, features } = this.props; + const { me, children, account, hasPatron, features, hasCrypto, cryptoLimit } = this.props; return (
@@ -44,6 +48,7 @@ class HomePage extends ImmutablePureComponent {
{hasPatron && } + {hasCrypto && }
diff --git a/app/styles/components/crypto-donate.scss b/app/styles/components/crypto-donate.scss index 10289e976..2fd6b7fd6 100644 --- a/app/styles/components/crypto-donate.scss +++ b/app/styles/components/crypto-donate.scss @@ -67,3 +67,35 @@ padding: 10px 0; } } + +.crypto-donate-panel { + &__message { + margin: 20px 0; + margin-top: -12px; + font-size: 14px; + } + + .site-wallet { + display: block; + padding-bottom: 10px; + } + + .crypto-address { + padding: 0; + margin: 20px 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + &--has-more { + .site-wallet { + padding-bottom: 0; + } + } +} diff --git a/app/styles/components/wtf-panel.scss b/app/styles/components/wtf-panel.scss index bad152f99..ce46a038a 100644 --- a/app/styles/components/wtf-panel.scss +++ b/app/styles/components/wtf-panel.scss @@ -129,4 +129,23 @@ } } } + + &__expand-btn { + display: block; + width: 100%; + height: 100%; + max-height: 46px; + position: relative; + border-top: 1px solid; + border-color: var(--brand-color--faint); + transition: max-height 150ms ease; + overflow: hidden; + opacity: 1; + text-align: center; + line-height: 46px; + font-size: 14px; + cursor: pointer; + color: var(--primary-text-color); + text-decoration: none; + } } From 6f1ce3847375967558b981c6927ebbf3cd4bc453 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Jun 2021 12:57:16 -0500 Subject: [PATCH 2/5] crypto_addresses -> cryptoAddresses --- .../features/crypto_donate/components/crypto_donate_panel.js | 2 +- app/soapbox/features/crypto_donate/components/site_wallet.js | 2 +- app/soapbox/pages/home_page.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js index b63acdad1..68005fac4 100644 --- a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js +++ b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js @@ -9,7 +9,7 @@ import { List as ImmutableList } from 'immutable'; import classNames from 'classnames'; const mapStateToProps = state => { - const addresses = state.getIn(['soapbox', 'crypto_addresses'], ImmutableList()); + const addresses = state.getIn(['soapbox', 'cryptoAddresses'], ImmutableList()); return { total: addresses.size, siteTitle: state.getIn(['instance', 'title']), diff --git a/app/soapbox/features/crypto_donate/components/site_wallet.js b/app/soapbox/features/crypto_donate/components/site_wallet.js index 5e5e3fa69..74edf711c 100644 --- a/app/soapbox/features/crypto_donate/components/site_wallet.js +++ b/app/soapbox/features/crypto_donate/components/site_wallet.js @@ -8,7 +8,7 @@ import CryptoAddress from './crypto_address'; const mapStateToProps = (state, ownProps) => { // Address example: // {"ticker": "btc", "address": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n", "note": "This is our main address"} - const addresses = state.getIn(['soapbox', 'crypto_addresses']); + const addresses = state.getIn(['soapbox', 'cryptoAddresses']); const { limit } = ownProps; return { diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js index 435752196..da90bed71 100644 --- a/app/soapbox/pages/home_page.js +++ b/app/soapbox/pages/home_page.js @@ -22,7 +22,7 @@ const mapStateToProps = state => { me, account: state.getIn(['accounts', me]), hasPatron: soapbox.getIn(['extensions', 'patron', 'enabled']), - hasCrypto: typeof soapbox.getIn(['crypto_addresses', 0, 'ticker']) === 'string', + hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string', cryptoLimit: soapbox.getIn(['cryptoDonatePanel', 'limit']), features: getFeatures(state.get('instance')), }; From 7525713460f62e85ed389940127194ca73c5a1bf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Jun 2021 13:48:43 -0500 Subject: [PATCH 3/5] CryptoDonate: configure wallets and panel in SoapboxConfig --- app/soapbox/actions/soapbox.js | 1 + .../components/crypto_donate_panel.js | 8 +++ app/soapbox/features/soapbox_config/index.js | 68 ++++++++++++++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 7e03a1e52..29a1ebf19 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -43,6 +43,7 @@ export const defaultConfig = ImmutableMap({ allowedEmoji: allowedEmoji, verifiedCanEditName: false, displayFqn: true, + cryptoAddresses: ImmutableList(), cryptoDonatePanel: ImmutableMap({ limit: 3, }), diff --git a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js index 68005fac4..7a8ac0fd2 100644 --- a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js +++ b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js @@ -28,11 +28,19 @@ class CryptoDonatePanel extends ImmutablePureComponent { limit: 3, } + shouldDisplay = () => { + const { limit, total } = this.props; + if (limit === 0 || total === 0) return false; + return true; + } + render() { const { limit, total, siteTitle } = this.props; const more = total - limit; const hasMore = more > 0; + if (!this.shouldDisplay()) return null; + return (
diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index cf97f73d3..dfcfffc32 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -9,6 +9,7 @@ import { SimpleForm, FieldsGroup, TextInput, + SimpleInput, SimpleTextarea, FileChooserLogo, FormPropTypes, @@ -28,15 +29,21 @@ import SitePreview from './components/site_preview'; import ThemeToggle from 'soapbox/features/ui/components/theme_toggle'; import { defaultSettings } from 'soapbox/actions/settings'; import IconPickerDropdown from './components/icon_picker_dropdown'; +import snackbar from 'soapbox/actions/snackbar'; const messages = defineMessages({ heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' }, + saved: { id: 'soapbox_config.saved', defaultMessage: 'Soapbox config saved!' }, copyrightFooterLabel: { id: 'soapbox_config.copyright_footer.meta_fields.label_placeholder', defaultMessage: 'Copyright footer' }, promoItemIcon: { id: 'soapbox_config.promo_panel.meta_fields.icon_placeholder', defaultMessage: 'Icon' }, promoItemLabel: { id: 'soapbox_config.promo_panel.meta_fields.label_placeholder', defaultMessage: 'Label' }, promoItemURL: { id: 'soapbox_config.promo_panel.meta_fields.url_placeholder', defaultMessage: 'URL' }, homeFooterItemLabel: { id: 'soapbox_config.home_footer.meta_fields.label_placeholder', defaultMessage: 'Label' }, homeFooterItemURL: { id: 'soapbox_config.home_footer.meta_fields.url_placeholder', defaultMessage: 'URL' }, + cryptoAdressItemTicker: { id: 'soapbox_config.crypto_address.meta_fields.ticker_placeholder', defaultMessage: 'Ticker' }, + cryptoAdressItemAddress: { id: 'soapbox_config.crypto_address.meta_fields.address_placeholder', defaultMessage: 'Address' }, + cryptoAdressItemNote: { id: 'soapbox_config.crypto_address.meta_fields.note_placeholder', defaultMessage: 'Note (optional)' }, + cryptoDonatePanelLimitLabel: { id: 'soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder', defaultMessage: 'Number of items to display in the crypto homepage widget' }, customCssLabel: { id: 'soapbox_config.custom_css.meta_fields.url_placeholder', defaultMessage: 'URL' }, rawJSONLabel: { id: 'soapbox_config.raw_json_label', defaultMessage: 'Advanced: Edit raw JSON data' }, rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Edit the settings data directly. Changes made directly to the JSON file will override the form fields above. Click "Save" to apply your changes.' }, @@ -49,6 +56,7 @@ const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const templates = { promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), footerItem: ImmutableMap({ title: '', url: '' }), + cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }), }; const mapStateToProps = state => ({ @@ -95,9 +103,10 @@ class SoapboxConfig extends ImmutablePureComponent { } handleSubmit = (event) => { - const { dispatch } = this.props; + const { dispatch, intl } = this.props; dispatch(updateConfig(this.getParams())).then(() => { this.setState({ isLoading: false }); + dispatch(snackbar.success(intl.formatMessage(messages.saved))); }).catch((error) => { this.setState({ isLoading: false }); }); @@ -158,6 +167,12 @@ class SoapboxConfig extends ImmutablePureComponent { ); }; + handleCryptoAdressItemChange = (index, key, field, getValue) => { + return this.handleItemChange( + ['cryptoAddresses', index], key, field, templates.cryptoAddress, getValue, + ); + }; + handleEditJSON = e => { this.setState({ rawJSON: e.target.value }); } @@ -323,6 +338,57 @@ class SoapboxConfig extends ImmutablePureComponent {
+ +
+ + + + + { + soapbox.get('cryptoAddresses').map((address, i) => ( +
+ + + + +
+ )) + } +
+
+ + +
+
+
+
+ + Number(e.target.value))} + /> + Date: Thu, 10 Jun 2021 14:00:45 -0500 Subject: [PATCH 4/5] CryptoDonate: add explanation box to donation page --- app/soapbox/features/crypto_donate/index.js | 35 +++++++++++++++++++-- app/styles/components/crypto-donate.scss | 4 +++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/crypto_donate/index.js b/app/soapbox/features/crypto_donate/index.js index a06652ffe..052b84774 100644 --- a/app/soapbox/features/crypto_donate/index.js +++ b/app/soapbox/features/crypto_donate/index.js @@ -1,15 +1,21 @@ import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Column from '../ui/components/column'; +import Accordion from 'soapbox/features/ui/components/accordion'; import SiteWallet from './components/site_wallet'; const messages = defineMessages({ heading: { id: 'column.crypto_donate', defaultMessage: 'Donate Cryptocurrency' }, }); -export default +const mapStateToProps = state => ({ + siteTitle: state.getIn(['instance', 'title']), +}); + +export default @connect(mapStateToProps) @injectIntl class CryptoDonate extends ImmutablePureComponent { @@ -17,12 +23,35 @@ class CryptoDonate extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; + state = { + explanationBoxExpanded: true, + } + + toggleExplanationBox = (setting) => { + this.setState({ explanationBoxExpanded: setting }); + } + render() { - const { intl } = this.props; + const { intl, siteTitle } = this.props; + const { explanationBoxExpanded } = this.state; return (
+
+ } + expanded={explanationBoxExpanded} + onToggle={this.toggleExplanationBox} + > + + + +
diff --git a/app/styles/components/crypto-donate.scss b/app/styles/components/crypto-donate.scss index 2fd6b7fd6..148500d70 100644 --- a/app/styles/components/crypto-donate.scss +++ b/app/styles/components/crypto-donate.scss @@ -1,3 +1,7 @@ +.crypto-donate { + padding-bottom: 10px; +} + .crypto-address { padding: 20px; display: flex; From ef53cd2de15cc59329fab8c24abdbc1ad1755684 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Jun 2021 14:05:07 -0500 Subject: [PATCH 5/5] CryptoDonate: add donate button to mobile sidebar --- app/soapbox/components/sidebar_menu.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index cfd774538..ce361134e 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -18,6 +18,7 @@ import { logOut, switchAccount } from 'soapbox/actions/auth'; import ThemeToggle from '../features/ui/components/theme_toggle_container'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; import { List as ImmutableList, is as ImmutableIs } from 'immutable'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const messages = defineMessages({ followers: { id: 'account.followers', defaultMessage: 'Followers' }, @@ -39,6 +40,7 @@ const messages = defineMessages({ apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' }, news: { id: 'tabs_bar.news', defaultMessage: 'News' }, donate: { id: 'donate', defaultMessage: 'Donate' }, + donate_crypto: { id: 'donate_crypto', defaultMessage: 'Donate cryptocurrency' }, info: { id: 'column.info', defaultMessage: 'Server information' }, add_account: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, }); @@ -46,6 +48,7 @@ const messages = defineMessages({ const mapStateToProps = state => { const me = state.get('me'); const getAccount = makeGetAccount(); + const soapbox = getSoapboxConfig(state); const otherAccounts = state @@ -61,6 +64,7 @@ const mapStateToProps = state => { account: getAccount(state, me), sidebarOpen: state.get('sidebar').sidebarOpen, donateUrl: state.getIn(['patron', 'instance', 'url']), + hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string', isStaff: isStaff(state.getIn(['accounts', me])), otherAccounts, }; @@ -153,7 +157,7 @@ class SidebarMenu extends ImmutablePureComponent { } render() { - const { sidebarOpen, intl, account, onClickLogOut, donateUrl, isStaff, otherAccounts } = this.props; + const { sidebarOpen, intl, account, onClickLogOut, donateUrl, isStaff, otherAccounts, hasCrypto } = this.props; const { switcher } = this.state; if (!account) return null; const acct = account.get('acct'); @@ -206,12 +210,14 @@ class SidebarMenu extends ImmutablePureComponent { {intl.formatMessage(messages.profile)} - {donateUrl ? - - - {intl.formatMessage(messages.donate)} - - : ''} + {donateUrl && + + {intl.formatMessage(messages.donate)} + } + {hasCrypto && + + {intl.formatMessage(messages.donate_crypto)} + } {intl.formatMessage(messages.lists)}