Merge branch 'next-sidebar' into 'next'
Next: Refactor sidebar panels, add back CryptoDonate features See merge request soapbox-pub/soapbox-fe!1149
This commit is contained in:
commit
88626a7206
|
@ -27,3 +27,4 @@ export { default as Tabs } from './tabs/tabs';
|
||||||
export { default as Text } from './text/text';
|
export { default as Text } from './text/text';
|
||||||
export { default as Textarea } from './textarea/textarea';
|
export { default as Textarea } from './textarea/textarea';
|
||||||
export { default as Tooltip } from './tooltip/tooltip';
|
export { default as Tooltip } from './tooltip/tooltip';
|
||||||
|
export { default as Widget } from './widget/widget';
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Stack, Text } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
interface IWidgetTitle {
|
||||||
|
title: string | React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const WidgetTitle = ({ title }: IWidgetTitle): JSX.Element => (
|
||||||
|
<Text size='xl' weight='bold' tag='h1'>{title}</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
const WidgetBody: React.FC = ({ children }): JSX.Element => (
|
||||||
|
<Stack space={3}>{children}</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IWidget {
|
||||||
|
title: string | React.ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Widget: React.FC<IWidget> = ({ title, children }): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack space={2}>
|
||||||
|
<WidgetTitle title={title} />
|
||||||
|
<WidgetBody>{children}</WidgetBody>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Widget;
|
|
@ -1,60 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import { CopyableInput } from 'soapbox/features/forms';
|
|
||||||
|
|
||||||
import { getExplorerUrl } from '../utils/block_explorer';
|
|
||||||
import CoinDB from '../utils/coin_db';
|
|
||||||
|
|
||||||
import CryptoIcon from './crypto_icon';
|
|
||||||
|
|
||||||
export default @connect()
|
|
||||||
class CryptoAddress extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
address: PropTypes.string.isRequired,
|
|
||||||
ticker: PropTypes.string.isRequired,
|
|
||||||
note: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModalClick = e => {
|
|
||||||
this.props.dispatch(openModal('CRYPTO_DONATE', this.props));
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { address, ticker, note } = this.props;
|
|
||||||
const title = CoinDB.getIn([ticker, 'name']);
|
|
||||||
const explorerUrl = getExplorerUrl(ticker, address);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='crypto-address'>
|
|
||||||
<div className='crypto-address__head'>
|
|
||||||
<CryptoIcon
|
|
||||||
className='crypto-address__icon'
|
|
||||||
ticker={ticker}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
|
|
||||||
<div className='crypto-address__actions'>
|
|
||||||
<a href='' onClick={this.handleModalClick}>
|
|
||||||
<Icon src={require('@tabler/icons/icons/qrcode.svg')} />
|
|
||||||
</a>
|
|
||||||
{explorerUrl && <a href={explorerUrl} target='_blank'>
|
|
||||||
<Icon src={require('@tabler/icons/icons/external-link.svg')} />
|
|
||||||
</a>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{note && <div className='crypto-address__note'>{note}</div>}
|
|
||||||
<div className='crypto-address__address simple_form'>
|
|
||||||
<CopyableInput value={address} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import { Text, Icon, Stack, HStack } from 'soapbox/components/ui';
|
||||||
|
import { CopyableInput } from 'soapbox/features/forms';
|
||||||
|
|
||||||
|
import { getExplorerUrl } from '../utils/block_explorer';
|
||||||
|
import { getTitle } from '../utils/coin_db';
|
||||||
|
|
||||||
|
import CryptoIcon from './crypto_icon';
|
||||||
|
|
||||||
|
interface ICryptoAddress {
|
||||||
|
address: string,
|
||||||
|
ticker: string,
|
||||||
|
note?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CryptoAddress: React.FC<ICryptoAddress> = (props): JSX.Element => {
|
||||||
|
const { address, ticker, note } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleModalClick = (e: React.MouseEvent<HTMLElement>): void => {
|
||||||
|
dispatch(openModal('CRYPTO_DONATE', props));
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = getTitle(ticker);
|
||||||
|
const explorerUrl = getExplorerUrl(ticker, address);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<HStack alignItems='center' className='mb-1'>
|
||||||
|
<CryptoIcon
|
||||||
|
className='flex items-start justify-center w-6 mr-2.5'
|
||||||
|
ticker={ticker}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text weight='bold'>{title || ticker.toUpperCase()}</Text>
|
||||||
|
|
||||||
|
<HStack alignItems='center' className='ml-auto'>
|
||||||
|
<a className='text-gray-500 ml-1' href='#' onClick={handleModalClick}>
|
||||||
|
<Icon src={require('@tabler/icons/icons/qrcode.svg')} />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{explorerUrl && (
|
||||||
|
<a className='text-gray-500 ml-1' href={explorerUrl} target='_blank'>
|
||||||
|
<Icon src={require('@tabler/icons/icons/external-link.svg')} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{note && (
|
||||||
|
<Text>{note}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='crypto-address__address simple_form'>
|
||||||
|
<CopyableInput value={address} />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CryptoAddress;
|
|
@ -1,76 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
|
|
||||||
import SiteWallet from './site_wallet';
|
|
||||||
|
|
||||||
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'>
|
|
||||||
<Icon src={require('@tabler/icons/icons/currency-bitcoin.svg')} className='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 our 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Text, Widget } from 'soapbox/components/ui';
|
||||||
|
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import SiteWallet from './site_wallet';
|
||||||
|
|
||||||
|
interface ICryptoDonatePanel {
|
||||||
|
limit: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Element | null => {
|
||||||
|
const addresses = useSoapboxConfig().get('cryptoAddresses');
|
||||||
|
const siteTitle = useAppSelector((state) => state.instance.title);
|
||||||
|
|
||||||
|
if (limit === 0 || addresses.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const more = addresses.size - limit;
|
||||||
|
const hasMore = more > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget title={<FormattedMessage id='crypto_donate_panel.heading' defaultMessage='Donate Cryptocurrency' />}>
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage
|
||||||
|
id='crypto_donate_panel.intro.message'
|
||||||
|
defaultMessage='{siteTitle} accepts cryptocurrency donations to fund our service. Thank you for your support!'
|
||||||
|
values={{ siteTitle }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<SiteWallet limit={limit} />
|
||||||
|
|
||||||
|
{hasMore && (
|
||||||
|
<Link className='wtf-panel__expand-btn' to='/donate/crypto'>
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage
|
||||||
|
id='crypto_donate_panel.actions.more'
|
||||||
|
defaultMessage='Click to see {count} more {count, plural, one {wallet} other {wallets}}'
|
||||||
|
values={{ count: more }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CryptoDonatePanel;
|
|
@ -1,34 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const getIcon = ticker => {
|
|
||||||
try {
|
|
||||||
return require(`cryptocurrency-icons/svg/color/${ticker.toLowerCase()}.svg`);
|
|
||||||
} catch {
|
|
||||||
return require('cryptocurrency-icons/svg/color/generic.svg');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class CryptoIcon extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
ticker: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
className: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { ticker, title, className } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('crypto-icon', className)}>
|
|
||||||
<img
|
|
||||||
src={getIcon(ticker)}
|
|
||||||
alt={title || ticker}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/** Get crypto icon URL by ticker symbol, or fall back to generic icon */
|
||||||
|
const getIcon = (ticker: string): string => {
|
||||||
|
try {
|
||||||
|
return require(`cryptocurrency-icons/svg/color/${ticker.toLowerCase()}.svg`);
|
||||||
|
} catch {
|
||||||
|
return require('cryptocurrency-icons/svg/color/generic.svg');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ICryptoIcon {
|
||||||
|
ticker: string,
|
||||||
|
title?: string,
|
||||||
|
className?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CryptoIcon: React.FC<ICryptoIcon> = ({ ticker, title, className }): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<img
|
||||||
|
src={getIcon(ticker)}
|
||||||
|
alt={title || ticker}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CryptoIcon;
|
|
@ -1,53 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import QRCode from 'qrcode.react';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import { CopyableInput } from 'soapbox/features/forms';
|
|
||||||
|
|
||||||
import { getExplorerUrl } from '../utils/block_explorer';
|
|
||||||
import CoinDB from '../utils/coin_db';
|
|
||||||
|
|
||||||
import CryptoIcon from './crypto_icon';
|
|
||||||
|
|
||||||
export default class DetailedCryptoAddress extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
address: PropTypes.string.isRequired,
|
|
||||||
ticker: PropTypes.string.isRequired,
|
|
||||||
note: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { address, ticker, note } = this.props;
|
|
||||||
const title = CoinDB.getIn([ticker, 'name']);
|
|
||||||
const explorerUrl = getExplorerUrl(ticker, address);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='crypto-address'>
|
|
||||||
<div className='crypto-address__head'>
|
|
||||||
<CryptoIcon
|
|
||||||
className='crypto-address__icon'
|
|
||||||
ticker={ticker}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
|
|
||||||
<div className='crypto-address__actions'>
|
|
||||||
{explorerUrl && <a href={explorerUrl} target='_blank'>
|
|
||||||
<Icon src={require('@tabler/icons/icons/external-link.svg')} />
|
|
||||||
</a>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{note && <div className='crypto-address__note'>{note}</div>}
|
|
||||||
<div className='crypto-address__qrcode'>
|
|
||||||
<QRCode value={address} />
|
|
||||||
</div>
|
|
||||||
<div className='crypto-address__address simple_form'>
|
|
||||||
<CopyableInput value={address} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import QRCode from 'qrcode.react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import { CopyableInput } from 'soapbox/features/forms';
|
||||||
|
|
||||||
|
import { getExplorerUrl } from '../utils/block_explorer';
|
||||||
|
import { getTitle } from '../utils/coin_db';
|
||||||
|
|
||||||
|
import CryptoIcon from './crypto_icon';
|
||||||
|
|
||||||
|
interface IDetailedCryptoAddress {
|
||||||
|
address: string,
|
||||||
|
ticker: string,
|
||||||
|
note?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DetailedCryptoAddress: React.FC<IDetailedCryptoAddress> = ({ address, ticker, note }): JSX.Element => {
|
||||||
|
const title = getTitle(ticker);
|
||||||
|
const explorerUrl = getExplorerUrl(ticker, address);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='crypto-address'>
|
||||||
|
<div className='crypto-address__head'>
|
||||||
|
<CryptoIcon
|
||||||
|
className='crypto-address__icon'
|
||||||
|
ticker={ticker}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
|
||||||
|
<div className='crypto-address__actions'>
|
||||||
|
{explorerUrl && <a href={explorerUrl} target='_blank'>
|
||||||
|
<Icon src={require('@tabler/icons/icons/external-link.svg')} />
|
||||||
|
</a>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{note && <div className='crypto-address__note'>{note}</div>}
|
||||||
|
<div className='crypto-address__qrcode'>
|
||||||
|
<QRCode value={address} />
|
||||||
|
</div>
|
||||||
|
<div className='crypto-address__address simple_form'>
|
||||||
|
<CopyableInput value={address} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailedCryptoAddress;
|
|
@ -1,67 +0,0 @@
|
||||||
import { trimStart } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import CryptoAddress from './crypto_address';
|
|
||||||
|
|
||||||
const normalizeAddress = address => {
|
|
||||||
return address.update('ticker', '', ticker => {
|
|
||||||
return trimStart(ticker, '$').toLowerCase();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAddresses = addresses => addresses.map(normalizeAddress);
|
|
||||||
|
|
||||||
const makeGetCoinList = () => {
|
|
||||||
return createSelector(
|
|
||||||
[(addresses, limit) => typeof limit === 'number' ? addresses.take(limit) : addresses],
|
|
||||||
addresses => normalizeAddresses(addresses),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getCoinList = makeGetCoinList();
|
|
||||||
|
|
||||||
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: getCoinList(addresses, limit),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(makeMapStateToProps)
|
|
||||||
class CoinList extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
coinList: ImmutablePropTypes.list,
|
|
||||||
limit: PropTypes.number,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { coinList } = this.props;
|
|
||||||
if (!coinList) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='site-wallet'>
|
|
||||||
{coinList.map(coin => (
|
|
||||||
<CryptoAddress
|
|
||||||
key={coin.get('ticker')}
|
|
||||||
{...coin.toJS()}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { trimStart } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Stack } from 'soapbox/components/ui';
|
||||||
|
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import CryptoAddress from './crypto_address';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
type Address = ImmutableMap<string, any>;
|
||||||
|
|
||||||
|
// Address example:
|
||||||
|
// {"ticker": "btc", "address": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n", "note": "This is our main address"}
|
||||||
|
const normalizeAddress = (address: Address): Address => {
|
||||||
|
return address.update('ticker', '', ticker => {
|
||||||
|
return trimStart(ticker, '$').toLowerCase();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ISiteWallet {
|
||||||
|
limit?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SiteWallet: React.FC<ISiteWallet> = ({ limit }): JSX.Element => {
|
||||||
|
const addresses: ImmutableList<Address> =
|
||||||
|
useSoapboxConfig().get('cryptoAddresses').map(normalizeAddress);
|
||||||
|
|
||||||
|
const coinList = typeof limit === 'number' ? addresses.take(limit) : addresses;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space={4}>
|
||||||
|
{coinList.map(coin => (
|
||||||
|
<CryptoAddress
|
||||||
|
key={coin.get('ticker')}
|
||||||
|
address={coin.get('address')}
|
||||||
|
ticker={coin.get('ticker')}
|
||||||
|
note={coin.get('note')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SiteWallet;
|
|
@ -1,64 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import Accordion from 'soapbox/features/ui/components/accordion';
|
|
||||||
|
|
||||||
import Column from '../ui/components/column';
|
|
||||||
|
|
||||||
import SiteWallet from './components/site_wallet';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
heading: { id: 'column.crypto_donate', defaultMessage: 'Donate Cryptocurrency' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
siteTitle: state.getIn(['instance', 'title']),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
@injectIntl
|
|
||||||
class CryptoDonate extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
explanationBoxExpanded: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleExplanationBox = (setting) => {
|
|
||||||
this.setState({ explanationBoxExpanded: setting });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { intl, siteTitle } = this.props;
|
|
||||||
const { explanationBoxExpanded } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column icon='bitcoin' heading={intl.formatMessage(messages.heading)}>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Column } from 'soapbox/components/ui';
|
||||||
|
import Accordion from 'soapbox/features/ui/components/accordion';
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import SiteWallet from './components/site_wallet';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.crypto_donate', defaultMessage: 'Donate Cryptocurrency' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const CryptoDonate: React.FC = (): JSX.Element => {
|
||||||
|
const [explanationBoxExpanded, toggleExplanationBox] = useState(true);
|
||||||
|
const siteTitle = useAppSelector((state) => state.instance.title);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column label={intl.formatMessage(messages.heading)} withHeader>
|
||||||
|
<div className='crypto-donate'>
|
||||||
|
<div className='explanation-box'>
|
||||||
|
<Accordion
|
||||||
|
headline={<FormattedMessage id='crypto_donate.explanation_box.title' defaultMessage='Sending cryptocurrency donations' />}
|
||||||
|
expanded={explanationBoxExpanded}
|
||||||
|
onToggle={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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CryptoDonate;
|
|
@ -1,7 +0,0 @@
|
||||||
import blockExplorers from './block_explorers.json';
|
|
||||||
|
|
||||||
export const getExplorerUrl = (ticker, address) => {
|
|
||||||
const template = blockExplorers[ticker];
|
|
||||||
if (!template) return false;
|
|
||||||
return template.replace('{address}', address);
|
|
||||||
};
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import blockExplorers from './block_explorers.json';
|
||||||
|
|
||||||
|
type BlockExplorers = Record<string, string | null>;
|
||||||
|
|
||||||
|
export const getExplorerUrl = (ticker: string, address: string): string | null => {
|
||||||
|
const template = (blockExplorers as BlockExplorers)[ticker];
|
||||||
|
if (!template) return null;
|
||||||
|
return template.replace('{address}', address);
|
||||||
|
};
|
|
@ -5,3 +5,9 @@ import manifestMap from './manifest_map';
|
||||||
// All this does is converts the result from manifest_map.js into an ImmutableMap
|
// All this does is converts the result from manifest_map.js into an ImmutableMap
|
||||||
const coinDB = fromJS(manifestMap);
|
const coinDB = fromJS(manifestMap);
|
||||||
export default coinDB;
|
export default coinDB;
|
||||||
|
|
||||||
|
/** Get title from CoinDB based on ticker symbol */
|
||||||
|
export const getTitle = (ticker: string): string => {
|
||||||
|
const title = coinDB.getIn([ticker, 'name']);
|
||||||
|
return typeof title === 'string' ? title : '';
|
||||||
|
};
|
|
@ -303,7 +303,7 @@ export class CopyableInput extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='copyable-input'>
|
<div className='copyable-input'>
|
||||||
<input ref={this.setInputRef} type='text' value={value} readOnly />
|
<input ref={this.setInputRef} type='text' value={value} readOnly />
|
||||||
<button onClick={this.handleCopyClick}>
|
<button className='p-2 text-white bg-primary-600' onClick={this.handleCopyClick}>
|
||||||
<FormattedMessage id='forms.copy' defaultMessage='Copy' />
|
<FormattedMessage id='forms.copy' defaultMessage='Copy' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,11 +3,11 @@ import * as React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { Widget } from 'soapbox/components/ui';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { fetchTrends } from '../../../actions/trends';
|
import { fetchTrends } from '../../../actions/trends';
|
||||||
import Hashtag from '../../../components/hashtag';
|
import Hashtag from '../../../components/hashtag';
|
||||||
import { Stack, Text } from '../../../components/ui';
|
|
||||||
|
|
||||||
interface ITrendsPanel {
|
interface ITrendsPanel {
|
||||||
limit: number
|
limit: number
|
||||||
|
@ -35,17 +35,11 @@ const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={2}>
|
<Widget title={<FormattedMessage id='trends.title' defaultMessage='Trends' />}>
|
||||||
<Text size='xl' weight='bold'>
|
{sortedTrends.map((hashtag: ImmutableMap<string, any>) => (
|
||||||
<FormattedMessage id='trends.title' defaultMessage='Trends' />
|
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
|
||||||
</Text>
|
))}
|
||||||
|
</Widget>
|
||||||
<Stack space={3}>
|
|
||||||
{sortedTrends.map((hashtag: ImmutableMap<string, any>) => (
|
|
||||||
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as React from 'react';
|
||||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { Stack, Text } from 'soapbox/components/ui';
|
import { Widget } from 'soapbox/components/ui';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
|
import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
|
||||||
|
@ -37,24 +37,18 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={2}>
|
<Widget title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}>
|
||||||
<Text size='xl' weight='bold'>
|
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => (
|
||||||
<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />
|
<AccountContainer
|
||||||
</Text>
|
key={suggestion.get('account')}
|
||||||
|
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
||||||
<Stack space={3}>
|
id={suggestion.get('account')}
|
||||||
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => (
|
actionIcon={require('@tabler/icons/icons/x.svg')}
|
||||||
<AccountContainer
|
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
|
||||||
key={suggestion.get('account')}
|
onActionClick={handleDismiss}
|
||||||
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
/>
|
||||||
id={suggestion.get('account')}
|
))}
|
||||||
actionIcon={require('@tabler/icons/icons/x.svg')}
|
</Widget>
|
||||||
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
|
|
||||||
onActionClick={handleDismiss}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ import {
|
||||||
// AwaitingApproval,
|
// AwaitingApproval,
|
||||||
// Reports,
|
// Reports,
|
||||||
// ModerationLog,
|
// ModerationLog,
|
||||||
// CryptoDonate,
|
CryptoDonate,
|
||||||
// ScheduledStatuses,
|
// ScheduledStatuses,
|
||||||
// UserIndex,
|
// UserIndex,
|
||||||
// FederationRestrictions,
|
// FederationRestrictions,
|
||||||
|
@ -347,8 +347,8 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
||||||
<WrappedRoute path='/error' page={EmptyPage} component={IntentionalError} content={children} />
|
<WrappedRoute path='/error' page={EmptyPage} component={IntentionalError} content={children} />
|
||||||
|
|
||||||
{/*
|
|
||||||
<WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />
|
<WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />
|
||||||
|
{/*
|
||||||
<WrappedRoute path='/federation_restrictions' publicRoute page={DefaultPage} component={FederationRestrictions} content={children} />
|
<WrappedRoute path='/federation_restrictions' publicRoute page={DefaultPage} component={FederationRestrictions} content={children} />
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
TrendsPanel,
|
TrendsPanel,
|
||||||
SignUpPanel,
|
SignUpPanel,
|
||||||
|
CryptoDonatePanel,
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
@ -31,10 +32,9 @@ const mapStateToProps = state => {
|
||||||
me,
|
me,
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
showFundingPanel: hasPatron,
|
showFundingPanel: hasPatron,
|
||||||
showCryptoDonatePanel: hasCrypto && cryptoLimit > 0,
|
hasCrypto,
|
||||||
cryptoLimit,
|
cryptoLimit,
|
||||||
showTrendsPanel: features.trends,
|
features,
|
||||||
showWhoToFollowPanel: features.suggestions,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class HomePage extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { me, children, account, showTrendsPanel, showWhoToFollowPanel } = this.props;
|
const { me, children, account, features, hasCrypto, cryptoLimit } = this.props;
|
||||||
|
|
||||||
const acct = account ? account.get('acct') : '';
|
const acct = account ? account.get('acct') : '';
|
||||||
|
|
||||||
|
@ -80,17 +80,22 @@ class HomePage extends ImmutablePureComponent {
|
||||||
<Layout.Aside>
|
<Layout.Aside>
|
||||||
{!me && (
|
{!me && (
|
||||||
<BundleContainer fetchComponent={SignUpPanel}>
|
<BundleContainer fetchComponent={SignUpPanel}>
|
||||||
{Component => <Component key='sign-up-panel' />}
|
{Component => <Component />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
)}
|
)}
|
||||||
{showTrendsPanel && (
|
{features.trends && (
|
||||||
<BundleContainer fetchComponent={TrendsPanel}>
|
<BundleContainer fetchComponent={TrendsPanel}>
|
||||||
{Component => <Component limit={3} key='trends-panel' />}
|
{Component => <Component limit={3} />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
)}
|
)}
|
||||||
{showWhoToFollowPanel && (
|
{hasCrypto && cryptoLimit > 0 && (
|
||||||
|
<BundleContainer fetchComponent={CryptoDonatePanel}>
|
||||||
|
{Component => <Component limit={cryptoLimit} />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
|
{features.suggestions && (
|
||||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||||
{Component => <Component limit={5} key='wtf-panel' />}
|
{Component => <Component limit={5} />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
)}
|
)}
|
||||||
<LinkFooter key='link-footer' />
|
<LinkFooter key='link-footer' />
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
"@types/escape-html": "^1.0.1",
|
"@types/escape-html": "^1.0.1",
|
||||||
"@types/http-link-header": "^1.0.3",
|
"@types/http-link-header": "^1.0.3",
|
||||||
"@types/lodash": "^4.14.180",
|
"@types/lodash": "^4.14.180",
|
||||||
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"@types/react-helmet": "^6.1.5",
|
"@types/react-helmet": "^6.1.5",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-toggle": "^4.0.3",
|
"@types/react-toggle": "^4.0.3",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"typeRoots": [ "./types", "./node_modules/@types"]
|
"typeRoots": [ "./types", "./node_modules/@types"]
|
||||||
|
|
|
@ -2095,6 +2095,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
|
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
|
||||||
integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==
|
integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==
|
||||||
|
|
||||||
|
"@types/qrcode.react@^1.0.2":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/qrcode.react/-/qrcode.react-1.0.2.tgz#f892432cc41b5dac52e3ca8873b717c8bfea6002"
|
||||||
|
integrity sha512-I9Oq5Cjlkgy3Tw7krCnCXLw2/zMhizkTere49OOcta23tkvH0xBTP0yInimTh0gstLRtb8Ki9NZVujE5UI6ffQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-helmet@^6.1.5":
|
"@types/react-helmet@^6.1.5":
|
||||||
version "6.1.5"
|
version "6.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.5.tgz#35f89a6b1646ee2bc342a33a9a6c8777933f9083"
|
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.5.tgz#35f89a6b1646ee2bc342a33a9a6c8777933f9083"
|
||||||
|
|
Loading…
Reference in New Issue