Merge branch 'crypto-donate-2' into 'develop'
Crypto donate improvements See merge request soapbox-pub/soapbox-fe!522
This commit is contained in:
commit
31c0ce802b
9 changed files with 258 additions and 15 deletions
|
@ -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…
Reference in a new issue