Merge branch 'crypto-donate-2' into 'develop'

Crypto donate improvements

See merge request soapbox-pub/soapbox-fe!522
merge-requests/523/head
Alex Gleason 3 years ago
commit 31c0ce802b

@ -43,6 +43,10 @@ export const defaultConfig = ImmutableMap({
allowedEmoji: allowedEmoji,
verifiedCanEditName: false,
displayFqn: true,
cryptoAddresses: ImmutableList(),
cryptoDonatePanel: ImmutableMap({
limit: 3,
}),
});
export function getSoapboxConfig(state) {

@ -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 {
<Icon id='user' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.profile)}</span>
</NavLink>
{donateUrl ?
<a className='sidebar-menu-item' href={donateUrl} onClick={this.handleClose}>
<Icon id='dollar' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
</a>
: ''}
{donateUrl && <a className='sidebar-menu-item' href={donateUrl} onClick={this.handleClose}>
<Icon id='dollar' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
</a>}
{hasCrypto && <NavLink className='sidebar-menu-item' to='/donate/crypto' onClick={this.handleClose}>
<Icon id='bitcoin' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate_crypto)}</span>
</NavLink>}
<NavLink className='sidebar-menu-item' to='/lists' onClick={this.handleClose}>
<Icon id='list' />
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>

@ -0,0 +1,73 @@
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', 'cryptoAddresses'], 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,
}
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 (
<div className={classNames('wtf-panel funding-panel crypto-donate-panel', { 'crypto-donate-panel--has-more': hasMore })}>
<div className='wtf-panel-header'>
<i role='img' alt='bitcoin' className='fa fa-bitcoin wtf-panel-header__icon' />
<span className='wtf-panel-header__label'>
<span><FormattedMessage id='crypto_donate_panel.heading' defaultMessage='Donate Cryptocurrency' /></span>
</span>
</div>
<div className='wtf-panel__content'>
<div className='crypto-donate-panel__message'>
<FormattedMessage
id='crypto_donate_panel.intro.message'
defaultMessage='{siteTitle} accepts cryptocurrency donations to fund the service. Thank you for your support!'
values={{ siteTitle }}
/>
</div>
<SiteWallet limit={limit} />
</div>
{hasMore && <Link className='wtf-panel__expand-btn' to='/donate/crypto'>
<FormattedMessage
id='crypto_donate_panel.actions.more'
defaultMessage='Click to see {count} more {count, plural, one {wallet} other {wallets}}'
values={{ count: more }}
/>
</Link>}
</div>
);
}
};

@ -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', 'cryptoAddresses']);
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() {

@ -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 (
<Column icon='bitcoin' heading={intl.formatMessage(messages.heading)} backBtnSlim>
<div className='crypto-donate'>
<div className='explanation-box'>
<Accordion
headline={<FormattedMessage id='crypto_donate.explanation_box.title' defaultMessage='Sending cryptocurrency donations' />}
expanded={explanationBoxExpanded}
onToggle={this.toggleExplanationBox}
>
<FormattedMessage
id='crypto_donate.explanation_box.message'
defaultMessage='{siteTitle} accepts cryptocurrency donations. You may send a donation to any of the addresses below. Thank you for your support!'
values={{ siteTitle }}
/>
</Accordion>
</div>
<SiteWallet />
</div>
</Column>

@ -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 {
</div>
</div>
</FieldsGroup>
<FieldsGroup>
<div className='input with_block_label'>
<label><FormattedMessage id='soapbox_config.fields.crypto_addresses_label' defaultMessage='Cryptocurrency addresses' /></label>
<span className='hint'>
<FormattedMessage id='soapbox_config.hints.crypto_addresses' defaultMessage='Add cryptocurrency addresses so users of your site can donate to you. Order matters, and you must use lowercase ticker values.' />
</span>
{
soapbox.get('cryptoAddresses').map((address, i) => (
<div className='row' key={i}>
<TextInput
label={intl.formatMessage(messages.cryptoAdressItemTicker)}
placeholder={intl.formatMessage(messages.cryptoAdressItemTicker)}
value={address.get('ticker')}
onChange={this.handleCryptoAdressItemChange(i, 'ticker', address)}
/>
<TextInput
label={intl.formatMessage(messages.cryptoAdressItemAddress)}
placeholder={intl.formatMessage(messages.cryptoAdressItemAddress)}
value={address.get('address')}
onChange={this.handleCryptoAdressItemChange(i, 'address', address)}
/>
<TextInput
label={intl.formatMessage(messages.cryptoAdressItemNote)}
placeholder={intl.formatMessage(messages.cryptoAdressItemNote)}
value={address.get('note')}
onChange={this.handleCryptoAdressItemChange(i, 'note', address)}
/>
<Icon id='times-circle' onClick={this.handleDeleteItem(['cryptoAddresses', i])} />
</div>
))
}
<div className='actions add-row'>
<div name='button' type='button' role='presentation' className='btn button button-secondary' onClick={this.handleAddItem(['cryptoAddresses'], templates.cryptoAddress)}>
<Icon id='plus-circle' />
<FormattedMessage id='soapbox_config.fields.crypto_address.add' defaultMessage='Add new crypto address' />
</div>
</div>
</div>
</FieldsGroup>
<FieldsGroup>
<SimpleInput
type='number'
min={0}
pattern='[0-9]+'
name='cryptoDonatePanelLimit'
label={intl.formatMessage(messages.cryptoDonatePanelLimitLabel)}
placeholder={intl.formatMessage(messages.cryptoDonatePanelLimitLabel)}
value={soapbox.getIn(['cryptoDonatePanel', 'limit'])}
onChange={this.handleChange(['cryptoDonatePanel', 'limit'], (e) => Number(e.target.value))}
/>
</FieldsGroup>
<Accordion
headline={intl.formatMessage(messages.rawJSONLabel)}
expanded={this.state.jsonEditorExpanded}

@ -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(['cryptoAddresses', 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 (
<div className='page'>
@ -44,6 +48,7 @@ class HomePage extends ImmutablePureComponent {
<div className='columns-area__panels__pane__inner'>
<UserPanel accountId={me} />
{hasPatron && <FundingPanel />}
{hasCrypto && <CryptoDonatePanel limit={cryptoLimit} />}
</div>
</div>

@ -1,3 +1,7 @@
.crypto-donate {
padding-bottom: 10px;
}
.crypto-address {
padding: 20px;
display: flex;
@ -67,3 +71,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;
}
}
}

@ -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;
}
}

Loading…
Cancel
Save