Merge branch 'crypto-donate' into 'develop'
Crypto donations See merge request soapbox-pub/soapbox-fe!521
This commit is contained in:
commit
a3ee5789bc
20 changed files with 408 additions and 8 deletions
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import CoinDB from '../utils/coin_db';
|
||||
import { getCoinIcon } from '../utils/coin_icons';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { CopyableInput } from 'soapbox/features/forms';
|
||||
import { getExplorerUrl } from '../utils/block_explorer';
|
||||
|
||||
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'>
|
||||
<div className='crypto-address__icon'>
|
||||
<img src={getCoinIcon(ticker)} alt={title} />
|
||||
</div>
|
||||
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
|
||||
<div className='crypto-address__actions'>
|
||||
<a href='' onClick={this.handleModalClick}>
|
||||
<Icon id='qrcode' />
|
||||
</a>
|
||||
{explorerUrl && <a href={explorerUrl} target='_blank'>
|
||||
<Icon id='external-link' />
|
||||
</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,50 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import QRCode from 'qrcode.react';
|
||||
import CoinDB from '../utils/coin_db';
|
||||
import { getCoinIcon } from '../utils/coin_icons';
|
||||
import { CopyableInput } from 'soapbox/features/forms';
|
||||
import { getExplorerUrl } from '../utils/block_explorer';
|
||||
|
||||
export default @connect()
|
||||
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'>
|
||||
<div className='crypto-address__icon'>
|
||||
<img src={getCoinIcon(ticker)} alt={title} />
|
||||
</div>
|
||||
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
|
||||
<div className='crypto-address__actions'>
|
||||
{explorerUrl && <a href={explorerUrl} target='_blank'>
|
||||
<Icon id='external-link' />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
38
app/soapbox/features/crypto_donate/components/site_wallet.js
Normal file
38
app/soapbox/features/crypto_donate/components/site_wallet.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import CryptoAddress from './crypto_address';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
// Address example:
|
||||
// {"ticker": "btc", "address": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n", "note": "This is our main address"}
|
||||
return {
|
||||
coinList: state.getIn(['soapbox', 'crypto_addresses']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class CoinList extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
coinList: ImmutablePropTypes.list,
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
32
app/soapbox/features/crypto_donate/index.js
Normal file
32
app/soapbox/features/crypto_donate/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Column from '../ui/components/column';
|
||||
import SiteWallet from './components/site_wallet';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.crypto_donate', defaultMessage: 'Donate Cryptocurrency' },
|
||||
});
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
class CryptoDonate extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Column icon='bitcoin' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<div className='crypto-donate'>
|
||||
<SiteWallet />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
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,8 @@
|
|||
{
|
||||
"bch": "https://explorer.bitcoin.com/bch/address/{address}",
|
||||
"btc": "https://explorer.bitcoin.com/btc/address/{address}",
|
||||
"doge": "https://dogechain.info/address/{address}",
|
||||
"eth": "https://etherscan.io/address/{address}",
|
||||
"ubq": "https://ubiqscan.io/address/{address}",
|
||||
"xmr": "https://monerohash.com/explorer/search?value={address}"
|
||||
}
|
6
app/soapbox/features/crypto_donate/utils/coin_db.js
Normal file
6
app/soapbox/features/crypto_donate/utils/coin_db.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { fromJS } from 'immutable';
|
||||
import manifestMap from './manifest_map';
|
||||
|
||||
// All this does is converts the result from manifest_map.js into an ImmutableMap
|
||||
const coinDB = fromJS(manifestMap);
|
||||
export default coinDB;
|
20
app/soapbox/features/crypto_donate/utils/coin_icons.js
Normal file
20
app/soapbox/features/crypto_donate/utils/coin_icons.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Does some trickery to import all the icons into the project
|
||||
// See: https://stackoverflow.com/questions/42118296/dynamically-import-images-from-a-directory-using-webpack
|
||||
|
||||
const icons = {};
|
||||
|
||||
function importAll(r) {
|
||||
const pathRegex = /\.\/(.*)\.svg/i;
|
||||
|
||||
r.keys().forEach((key) => {
|
||||
const ticker = pathRegex.exec(key)[1];
|
||||
return icons[ticker] = r(key).default;
|
||||
});
|
||||
}
|
||||
|
||||
importAll(require.context('cryptocurrency-icons/svg/color/', true, /\.svg$/));
|
||||
|
||||
export default icons;
|
||||
|
||||
// For getting the icon
|
||||
export const getCoinIcon = ticker => icons[ticker] || icons.generic || null;
|
12
app/soapbox/features/crypto_donate/utils/manifest_map.js
Normal file
12
app/soapbox/features/crypto_donate/utils/manifest_map.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
// @preval
|
||||
// Converts cryptocurrency-icon's manifest file from a list to a map.
|
||||
// See: https://github.com/spothq/cryptocurrency-icons/blob/master/manifest.json
|
||||
|
||||
const manifest = require('cryptocurrency-icons/manifest.json');
|
||||
const { Map: ImmutableMap, fromJS } = require('immutable');
|
||||
|
||||
const manifestMap = fromJS(manifest).reduce((acc, entry) => {
|
||||
return acc.set(entry.get('symbol').toLowerCase(), entry);
|
||||
}, ImmutableMap());
|
||||
|
||||
module.exports = manifestMap.toJS();
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
@ -264,3 +265,38 @@ export const FileChooserLogo = props => (
|
|||
FileChooserLogo.defaultProps = {
|
||||
accept: ['image/svg', 'image/png'],
|
||||
};
|
||||
|
||||
|
||||
export class CopyableInput extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
}
|
||||
|
||||
setInputRef = c => {
|
||||
this.input = c;
|
||||
}
|
||||
|
||||
handleCopyClick = e => {
|
||||
if (!this.input) return;
|
||||
|
||||
this.input.select();
|
||||
this.input.setSelectionRange(0, 99999);
|
||||
|
||||
document.execCommand('copy');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value } = this.props;
|
||||
|
||||
return (
|
||||
<div className='copyable-input'>
|
||||
<input ref={this.setInputRef} type='text' value={value} readOnly />
|
||||
<button onClick={this.handleCopyClick}>
|
||||
<FormattedMessage id='forms.copy' defaultMessage='Copy' />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
21
app/soapbox/features/ui/components/crypto_donate_modal.js
Normal file
21
app/soapbox/features/ui/components/crypto_donate_modal.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DetailedCryptoAddress from 'soapbox/features/crypto_donate/components/detailed_crypto_address';
|
||||
|
||||
export default class CryptoDonateModal extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
ticker: PropTypes.string.isRequired,
|
||||
note: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='modal-root__modal crypto-donate-modal'>
|
||||
<DetailedCryptoAddress {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -13,6 +13,7 @@ import FocalPointModal from './focal_point_modal';
|
|||
import HotkeysModal from './hotkeys_modal';
|
||||
import ComposeModal from './compose_modal';
|
||||
import UnauthorizedModal from './unauthorized_modal';
|
||||
import CryptoDonateModal from './crypto_donate_modal';
|
||||
|
||||
import {
|
||||
MuteModal,
|
||||
|
@ -37,6 +38,7 @@ const MODAL_COMPONENTS = {
|
|||
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
|
||||
'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
|
||||
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
|
||||
'CRYPTO_DONATE': () => Promise.resolve({ default: CryptoDonateModal }),
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
|
|
@ -13,6 +13,12 @@ import { List as ImmutableList } from 'immutable';
|
|||
import { getAcct, isAdmin, isModerator } from 'soapbox/utils/accounts';
|
||||
import { displayFqn } from 'soapbox/utils/state';
|
||||
import classNames from 'classnames';
|
||||
import CryptoAddress from 'soapbox/features/crypto_donate/components/crypto_address';
|
||||
|
||||
const TICKER_REGEX = /\$([a-zA-Z]*)/i;
|
||||
|
||||
const getTicker = value => (value.match(TICKER_REGEX) || [])[1];
|
||||
const isTicker = value => Boolean(getTicker(value));
|
||||
|
||||
const messages = defineMessages({
|
||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||
|
@ -123,15 +129,19 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
|||
</dl>
|
||||
))}
|
||||
|
||||
{fields.map((pair, i) => (
|
||||
<dl className='profile-info-panel-content__fields__item' key={i}>
|
||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
|
||||
{fields.map((pair, i) =>
|
||||
isTicker(pair.get('name', '')) ? (
|
||||
<CryptoAddress ticker={getTicker(pair.get('name')).toLowerCase()} address={pair.get('value_plain')} />
|
||||
) : (
|
||||
<dl className='profile-info-panel-content__fields__item' key={i}>
|
||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
|
||||
|
||||
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
|
||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
|
||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
||||
</dd>
|
||||
</dl>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -92,6 +92,7 @@ import {
|
|||
AwaitingApproval,
|
||||
Reports,
|
||||
ModerationLog,
|
||||
CryptoDonate,
|
||||
} from './util/async-components';
|
||||
|
||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
|
@ -289,6 +290,8 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/admin/log' page={AdminPage} component={ModerationLog} content={children} exact />
|
||||
<WrappedRoute path='/info' layout={LAYOUT.EMPTY} component={ServerInfo} content={children} />
|
||||
|
||||
<WrappedRoute path='/donate/crypto' layout={LAYOUT.DEFAULT} component={CryptoDonate} content={children} />
|
||||
|
||||
<WrappedRoute layout={LAYOUT.EMPTY} component={GenericNotFound} content={children} />
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -229,3 +229,7 @@ export function Reports() {
|
|||
export function ModerationLog() {
|
||||
return import(/* webpackChunkName: "features/admin/moderation_log" */'../../admin/moderation_log');
|
||||
}
|
||||
|
||||
export function CryptoDonate() {
|
||||
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate');
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
@import 'components/server-info';
|
||||
@import 'components/admin';
|
||||
@import 'components/backups';
|
||||
@import 'components/crypto-donate';
|
||||
|
||||
// Holiday
|
||||
@import 'holiday/halloween';
|
||||
|
|
69
app/styles/components/crypto-donate.scss
Normal file
69
app/styles/components/crypto-donate.scss
Normal file
|
@ -0,0 +1,69 @@
|
|||
.crypto-address {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
margin-right: 10px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
margin-left: auto;
|
||||
|
||||
a {
|
||||
color: var(--primary-text-color--faint);
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__note {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__qrcode {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__address {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.site-wallet {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.crypto-donate-modal {
|
||||
background: var(--foreground-color);
|
||||
border-radius: 8px;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
|
||||
.profile-info-panel-content__fields {
|
||||
.crypto-address {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
|
@ -788,3 +788,23 @@ code {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyable-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
font-size: 14px !important;
|
||||
border-radius: 4px 0 0 4px !important;
|
||||
}
|
||||
|
||||
button {
|
||||
width: auto;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding-bottom: 9px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
"classnames": "^2.2.5",
|
||||
"compression-webpack-plugin": "^6.0.2",
|
||||
"copy-webpack-plugin": "6.4.0",
|
||||
"cryptocurrency-icons": "^0.17.2",
|
||||
"css-loader": "^4.3.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"detect-passive-events": "^2.0.0",
|
||||
|
|
|
@ -3866,6 +3866,11 @@ crypto-browserify@^3.11.0:
|
|||
randombytes "^2.0.0"
|
||||
randomfill "^1.0.3"
|
||||
|
||||
cryptocurrency-icons@^0.17.2:
|
||||
version "0.17.2"
|
||||
resolved "https://registry.yarnpkg.com/cryptocurrency-icons/-/cryptocurrency-icons-0.17.2.tgz#25811b450d8698e7985bc91005d89555f13e6686"
|
||||
integrity sha512-301lellubLNhxkySIBNNG3VD05rWfMR+CFgo9LoLfuNybG2OLy0mpWduxv65WZkJpLl9hhpaVAxCV5SYbG5o9A==
|
||||
|
||||
css-color-names@0.0.4, css-color-names@^0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
|
||||
|
|
Loading…
Reference in a new issue