Introduce higher-level ApiObservables, use stakingBalance displays (#210)

* Move observables to ui-react-rx

* Move observables into ApiObservable

* Allow AddressSummary to render passed-in balance(s)

* Render Balance consistently in summaries

* Style Mini balance

* Use ApiObservable via HOC

* Nominee balances expanded

* Render nominators for account

* Add full balance when intending
This commit is contained in:
Jaco Greeff 2018-08-08 14:57:09 +02:00 committed by GitHub
parent 759ff2d5f0
commit 4d984d9f72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 464 additions and 256 deletions

View File

@ -7,6 +7,7 @@ import { Extrinsics } from '@polkadot/extrinsics/types';
import { SectionItem } from '@polkadot/params/types';
import { RawParam$Value } from '@polkadot/ui-app/Params/types';
import { QueueTx$ExtrinsicAdd } from '@polkadot/ui-signer/types';
import { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
import BN from 'bn.js';
import React from 'react';
@ -29,8 +30,9 @@ import translate from './translate';
type Props = I18nProps & {
accountIndex?: BN,
address: string,
balances: ExtendedBalanceMap,
name: string,
nominating?: string,
nominee?: string,
nominatorsFor?: Array<string>,
intentions: Array<string>,
isValidator: boolean,
@ -52,7 +54,7 @@ class Account extends React.PureComponent<Props, State> {
}
render () {
const { address, className, intentions, isValidator, name, nominating, style } = this.props;
const { address, className, intentions, isValidator, name, style } = this.props;
const { isNominateOpen } = this.state;
return (
@ -67,6 +69,7 @@ class Account extends React.PureComponent<Props, State> {
intentions={intentions}
/>
<AddressSummary
balance={this.balanceArray(address)}
name={name}
value={address}
>
@ -77,17 +80,58 @@ class Account extends React.PureComponent<Props, State> {
size='large'
/>
{this.renderButtons()}
<AddressMini value={nominating} />
{this.renderNominee()}
{this.renderNominators()}
</div>
</AddressSummary>
</div>
);
}
private balanceArray (address: string): Array<BN> | undefined {
const { balances } = this.props;
return balances[address]
? [balances[address].stakingBalance, balances[address].nominatedBalance]
: undefined;
}
private renderNominee () {
const { nominee } = this.props;
if (!nominee) {
return null;
}
return (
<AddressMini
balance={this.balanceArray(nominee)}
value={nominee}
withBalance
/>
);
}
private renderNominators () {
const { nominatorsFor } = this.props;
if (!nominatorsFor) {
return null;
}
return nominatorsFor.map((nominator) => (
<AddressMini
key={nominator}
value={nominator}
withBalance
/>
));
}
private renderButtons () {
const { address, intentions, nominating, t } = this.props;
const { address, intentions, nominee, t } = this.props;
const isIntending = intentions.includes(address);
const isNominating = !!nominating;
const isNominating = !!nominee;
const canStake = !isIntending && !isNominating;
if (canStake) {
@ -120,7 +164,7 @@ class Account extends React.PureComponent<Props, State> {
<Button.Group>
<UnnominateButton
address={address || ''}
nominating={nominating || ''}
nominating={nominee || ''}
onClick={this.unnominate}
/>
</Button.Group>
@ -199,7 +243,7 @@ export default withMulti(
withStorage(
storage.staking.public.nominating,
{
propName: 'nominating',
propName: 'nominee',
paramProp: 'address',
transform: encodeAddress
}

View File

@ -4,6 +4,7 @@
import { I18nProps } from '@polkadot/ui-app/types';
import { QueueProps } from '@polkadot/ui-signer/types';
import { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
import React from 'react';
import classes from '@polkadot/ui-app/util/classes';
@ -14,13 +15,14 @@ import Account from './Account';
import translate from './translate';
type Props = I18nProps & {
balances: ExtendedBalanceMap,
intentions: Array<string>,
validators: Array<string>
};
class StakeList extends React.PureComponent<Props> {
render () {
const { className, intentions, style, validators } = this.props;
const { balances, className, intentions, style, validators } = this.props;
return (
<QueueConsumer>
@ -36,6 +38,7 @@ class StakeList extends React.PureComponent<Props> {
return (
<Account
address={address}
balances={balances}
intentions={intentions}
isValidator={validators.includes(address)}
key={address}

View File

@ -4,8 +4,7 @@
import { Header } from '@polkadot/primitives/header';
import { I18nProps } from '@polkadot/ui-app/types';
import { ApiProps } from '@polkadot/ui-react-rx/types';
import { Balance, BalanceMap } from './observables/types';
import { ApiProps, ExtendedBalance, ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
import BN from 'bn.js';
import React from 'react';
@ -17,10 +16,10 @@ import withMulti from '@polkadot/ui-react-rx/with/multi';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
import validatingBalances from './observables/validatingBalances';
import translate from './translate';
type Props = ApiProps & I18nProps & {
balances: ExtendedBalanceMap,
intentions: Array<string>,
lastBlockHeader?: Header,
lastLengthChange?: BN,
@ -28,60 +27,11 @@ type Props = ApiProps & I18nProps & {
validators: Array<string>
};
type State = {
balances: BalanceMap,
subBalances: any
};
const DEFAULT_BLOCKNUMBER = new BN(0);
const DEFAULT_SESSION_CHANGE = new BN(0);
const DEFAULT_SESSION_LENGTH = new BN(60);
class Summary extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
balances: {},
subBalances: null
};
}
componentDidUpdate (prevProps: Props) {
const { intentions } = this.props;
if (intentions !== prevProps.intentions) {
this.subscribeBalances(intentions);
}
}
private unsubscribeBalances () {
const { subBalances } = this.state;
if (subBalances) {
subBalances.unsubscribe();
}
}
private subscribeBalances (accounts: string[] = []) {
const { api } = this.props;
this.unsubscribeBalances();
// FIXME: We want these as a HOC on the class
const subBalances = validatingBalances(api, accounts).subscribe((balances: BalanceMap) => {
this.setState({ balances });
});
this.setState({
subBalances
} as State);
}
componentWillUnmount () {
this.unsubscribeBalances();
}
class Summary extends React.PureComponent<Props> {
render () {
const { className, intentions, lastBlockHeader, lastLengthChange = DEFAULT_SESSION_CHANGE, style, sessionLength = DEFAULT_SESSION_LENGTH, t, validators } = this.props;
const blockNumber = lastBlockHeader
@ -130,11 +80,10 @@ class Summary extends React.PureComponent<Props, State> {
);
}
private calcIntentionsHigh (): Balance | null {
const { intentions, validators } = this.props;
const { balances } = this.state;
private calcIntentionsHigh (): ExtendedBalance | null {
const { balances, intentions, validators } = this.props;
return intentions.reduce((high: Balance | null, addr) => {
return intentions.reduce((high: ExtendedBalance | null, addr) => {
const balance = validators.includes(addr) || !balances[addr]
? null
: balances[addr];
@ -147,11 +96,10 @@ class Summary extends React.PureComponent<Props, State> {
}, null);
}
private calcValidatorLow (): Balance | null {
const { validators } = this.props;
const { balances } = this.state;
private calcValidatorLow (): ExtendedBalance | null {
const { balances, validators } = this.props;
return validators.reduce((low: Balance | null, addr) => {
return validators.reduce((low: ExtendedBalance | null, addr) => {
const balance = balances[addr] || null;
if (low === null || (balance && low.stakingBalance.gt(balance.stakingBalance))) {

View File

@ -3,11 +3,13 @@
// of the ISC license. See the LICENSE file for details.
import { BareProps } from '@polkadot/ui-app/types';
import { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
import React from 'react';
import storage from '@polkadot/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import classes from '@polkadot/ui-app/util/classes';
import withApiObservable from '@polkadot/ui-react-rx/with/apiObservable';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import withMulti from '@polkadot/ui-react-rx/with/multi';
@ -17,6 +19,7 @@ import StakeList from './StakeList';
import Summary from './Summary';
type Props = BareProps & {
balances?: ExtendedBalanceMap,
intentions?: Array<string>,
validators?: Array<string>
};
@ -26,7 +29,7 @@ const transformAddresses = (publicKeys: Array<Uint8Array>) =>
class App extends React.PureComponent<Props> {
render () {
const { className, intentions = [], style, validators = [] } = this.props;
const { balances = {}, className, intentions = [], style, validators = [] } = this.props;
return (
<div
@ -34,10 +37,12 @@ class App extends React.PureComponent<Props> {
style={style}
>
<Summary
balances={balances}
intentions={intentions}
validators={validators}
/>
<StakeList
balances={balances}
intentions={intentions}
validators={validators}
/>
@ -61,5 +66,12 @@ export default withMulti(
propName: 'validators',
transform: transformAddresses
}
),
withApiObservable(
'validatingBalances',
{
paramProp: 'intentions',
propName: 'balances'
}
)
);

View File

@ -1,19 +0,0 @@
// Copyright 2017-2018 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import BN from 'bn.js';
export type Balance = {
address: string,
freeBalance: BN,
nominatedBalance: BN,
reservedBalance: BN,
votingBalance: BN,
stakingBalance: BN,
nominators?: Array<Balance>
}
export type BalanceMap = {
[index: string]: Balance
}

View File

@ -1,45 +0,0 @@
// Copyright 2017-2018 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { RxApiInterface } from '@polkadot/api-rx/types';
import { Balance } from './types';
import BN from 'bn.js';
import { Observable, combineLatest } from 'rxjs';
import { map, switchMap, toArray } from 'rxjs/operators';
import storage from '@polkadot/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import votingBalance from './votingBalance';
import votingBalances from './/votingBalances';
type Result = [Balance, [Array<Balance>]];
export default function validatingBalance (api: RxApiInterface, address: string): Observable<Balance> {
return combineLatest(
votingBalance(api, address),
api.state
.getStorage(storage.staking.public.nominatorsFor, address)
.pipe(
switchMap((nominators = []) =>
// FIXME: Will not be needed after https://github.com/polkadot-js/apps/issues/128
votingBalances(api, nominators.map(encodeAddress))
),
toArray()
)
).pipe(
map(([balance, [nominators = []]]: Result): Balance => {
const nominatedBalance = nominators.reduce((total, nominator: Balance) => {
return total.add(nominator.votingBalance);
}, new BN(0));
return {
...balance,
nominators,
nominatedBalance,
stakingBalance: nominatedBalance.add(balance.votingBalance)
};
})
);
}

View File

@ -1,27 +0,0 @@
// Copyright 2017-2018 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { RxApiInterface } from '@polkadot/api-rx/types';
import { Balance, BalanceMap } from './types';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import validatingBalance from './validatingBalance';
export default function validatingBalances (api: RxApiInterface, addresses: Array<string>): Observable<BalanceMap> {
return combineLatest(
addresses.map((address) =>
validatingBalance(api, address)
)
).pipe(
map((result: Array<Balance>): BalanceMap => {
return result.reduce((balances, balance) => {
balances[balance.address] = balance;
return balances;
}, {} as BalanceMap);
})
);
}

View File

@ -1,27 +0,0 @@
// Copyright 2017-2018 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { RxApiInterface } from '@polkadot/api-rx/types';
import { Balance } from './types';
import BN from 'bn.js';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import storage from '@polkadot/storage';
export default function votingBalance (api: RxApiInterface, address: string): Observable<Balance> {
return combineLatest(
api.state.getStorage(storage.staking.public.freeBalanceOf, address),
api.state.getStorage(storage.staking.public.reservedBalanceOf, address)
).pipe(
map(([freeBalance = new BN(0), reservedBalance = new BN(0)]): Balance => ({
address,
freeBalance,
nominatedBalance: new BN(0),
reservedBalance,
stakingBalance: new BN(0),
votingBalance: freeBalance.add(reservedBalance)
}))
);
}

View File

@ -1,18 +0,0 @@
// Copyright 2017-2018 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { RxApiInterface } from '@polkadot/api-rx/types';
import { Balance } from './types';
import { Observable, combineLatest } from 'rxjs';
import votingBalance from './votingBalance';
export default function votingBalances (api: RxApiInterface, addresses: Array<string>): Observable<Balance[]> {
return combineLatest(
addresses.map((address) =>
votingBalance(api, address)
)
);
}

View File

@ -4,19 +4,22 @@
import { BareProps } from '@polkadot/ui-app/types';
import BN from 'bn.js';
import React from 'react';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import classes from './util/classes';
import toShortAddress from './util/toShortAddress';
import translate from './translate';
import Balance from './Balance';
type Props = BareProps & {
balance?: BN | Array<BN>,
isShort?: boolean,
value?: string
value?: string,
withBalance?: boolean
};
class AddressMini extends React.PureComponent<Props> {
export default class AddressMini extends React.PureComponent<Props> {
render () {
const { className, isShort = true, style, value } = this.props;
@ -24,21 +27,36 @@ class AddressMini extends React.PureComponent<Props> {
return null;
}
const shortValue = toShortAddress(value);
return (
<div
className={classes('ui--AddressMini', className)}
style={style}
>
<IdentityIcon
size={24}
value={value}
/>
<div>{isShort ? shortValue : value}</div>
<div className='ui--AddressMini-info'>
<IdentityIcon
size={24}
value={value}
/>
<div>{isShort ? toShortAddress(value) : value}</div>
</div>
{this.renderBalance()}
</div>
);
}
}
export default translate(AddressMini);
private renderBalance () {
const { balance, value, withBalance = false } = this.props;
if (!withBalance || !value) {
return null;
}
return (
<Balance
balance={balance}
className='ui--AddressSummary-balance'
value={value}
/>
);
}
}

View File

@ -29,6 +29,7 @@ class AddressRow extends AddressSummary {
<div className='ui--AddressRow-details'>
{this.renderAddress()}
{this.renderBalance()}
{this.renderNonce()}
</div>
</div>
{this.renderChildren()}

View File

@ -4,20 +4,21 @@
import { I18nProps } from '@polkadot/ui-app/types';
import BN from 'bn.js';
import React from 'react';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import Balance from '@polkadot/ui-react-rx/Balance';
import Nonce from '@polkadot/ui-react-rx/Nonce';
import addressDecode from '@polkadot/util-keyring/address/decode';
import addressEncode from '@polkadot/util-keyring/address/encode';
import classes from './util/classes';
import toShortAddress from './util/toShortAddress';
import Balance from './Balance';
import CopyButton from './CopyButton';
import translate from './translate';
export type Props = I18nProps & {
balance?: BN | Array<BN>,
children?: React.ReactNode,
name?: string,
value: string,
@ -74,6 +75,7 @@ class AddressSummary extends React.PureComponent<Props, State> {
/>
{this.renderAddress()}
{this.renderBalance()}
{this.renderNonce()}
</div>
{this.renderChildren()}
</div>
@ -99,40 +101,42 @@ class AddressSummary extends React.PureComponent<Props, State> {
protected renderBalance () {
const { isValid, publicKey } = this.state;
const { balance, t, withBalance = true } = this.props;
if (!isValid) {
if (!withBalance || !isValid || !publicKey) {
return null;
}
const { t, withBalance = true, withNonce = true } = this.props;
return (
<Balance
balance={balance}
className='ui--AddressSummary-balance'
label={t('addressSummary.balance', {
defaultValue: 'balance '
})}
value={publicKey}
/>
);
}
return [
withBalance
? (
<Balance
className='ui--AddressSummary-balance'
key='balance'
label={t('addressSummary.balance', {
defaultValue: 'balance '
})}
params={publicKey}
/>
)
: null,
withNonce
? (
<Nonce
className='ui--AddressSummary-nonce'
key='nonce'
params={publicKey}
>
{t('addressSummary.transactions', {
defaultValue: ' transactions'
})}
</Nonce>
)
: null
];
protected renderNonce () {
const { isValid, publicKey } = this.state;
const { t, withNonce = true } = this.props;
if (!withNonce || !isValid) {
return null;
}
return (
<Nonce
className='ui--AddressSummary-nonce'
params={publicKey}
>
{t('addressSummary.transactions', {
defaultValue: ' transactions'
})}
</Nonce>
);
}
protected renderChildren () {

View File

@ -0,0 +1,66 @@
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { BareProps } from '@polkadot/ui-app/types';
import BN from 'bn.js';
import React from 'react';
import RxBalance from '@polkadot/ui-react-rx/Balance';
import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
import classes from './util/classes';
import translate from './translate';
export type Props = BareProps & {
balance?: BN | Array<BN>,
label?: string,
value: string | Uint8Array,
withLabel?: boolean
};
class Balance extends React.PureComponent<Props> {
render () {
const { balance, className, label, value, style } = this.props;
return balance
? this.renderProvided()
: (
<RxBalance
className={classes('ui--Balance', className)}
label={label}
params={value}
style={style}
/>
);
}
private renderProvided () {
const { balance, className, label, style } = this.props;
if (!balance) {
return null;
}
let value = `${numberFormat(Array.isArray(balance) ? balance[0] : balance)}`;
if (Array.isArray(balance)) {
const totals = balance
.filter((value, index) => index !== 0)
.map(numberFormat);
value = `${value} (${totals.length === 1 ? '+' : ''}${totals.join(', ')})`;
}
return (
<div
className={classes('ui--Balance', className)}
style={style}
>
{label}{value}
</div>
);
}
}
export default translate(Balance);

View File

@ -36,16 +36,22 @@
padding: 0.25rem;
}
.ui--AddressMini > div {
.ui--AddressMini-info div {
display: inline-block;
vertical-align: middle;
}
.ui--AddressMini > .ui--IdentityIcon {
.ui--AddressMini .ui--IdentityIcon {
margin-left: 1rem;
margin-right: 0.5rem;
}
.ui--AddressMini .ui--Balance {
font-size: 0.75rem;
margin-top: -0.5rem;
text-align: right;
}
.ui--AddressRow {
display: flex;
width: 100%;
@ -88,6 +94,7 @@
display: block
}
.ui--Balance,
.ui--AddressSummary-balance,
.ui--AddressSummary-nonce {
opacity: 0.45;

View File

@ -14,6 +14,7 @@ import WsProvider from '@polkadot/api-provider/ws';
import createApi from '@polkadot/api-rx';
import defaults from '@polkadot/api-rx/defaults';
import ApiObservable from '../ApiObservable';
import ApiContext from './Context';
type Props = {
@ -44,7 +45,9 @@ export default class Api extends React.PureComponent<Props, State> {
: provider
);
const setApi = (api: RxApiInterface): void => {
this.setState({ api }, () => {
const apiObservable = new ApiObservable(api);
this.setState({ api, apiObservable }, () => {
this.updateSubscriptions();
});
};
@ -56,6 +59,7 @@ export default class Api extends React.PureComponent<Props, State> {
this.state = {
api,
apiConnected: false,
apiObservable: new ApiObservable(api),
apiSupport: 'poc-1',
setApi,
setApiProvider,
@ -111,12 +115,13 @@ export default class Api extends React.PureComponent<Props, State> {
}
render () {
const { api, apiConnected, apiSupport, setApi, setApiProvider, setApiWsUrl } = this.state;
const { api, apiConnected, apiObservable, apiSupport, setApi, setApiProvider, setApiWsUrl } = this.state;
return (
<ApiContext.Provider value={{
api,
apiConnected,
apiObservable,
apiSupport,
setApi,
setApiProvider,

View File

@ -0,0 +1,88 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { RxApiInterface } from '@polkadot/api-rx/types';
import { ExtendedBalance, ExtendedBalanceMap, ObservableApiInterface } from './types';
import BN from 'bn.js';
import { Observable, combineLatest } from 'rxjs';
import { map, switchMap, toArray } from 'rxjs/operators';
import storage from '@polkadot/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
export default class ObservableApi implements ObservableApiInterface {
private api: RxApiInterface;
constructor (api: RxApiInterface) {
this.api = api;
}
validatingBalance = (address: string): Observable<ExtendedBalance> => {
return combineLatest(
this.votingBalance(address),
this.api.state
.getStorage(storage.staking.public.nominatorsFor, address)
.pipe(
switchMap((nominators = []) =>
// FIXME: Will not be needed after https://github.com/polkadot-js/apps/issues/128
this.votingBalances(nominators.map(encodeAddress))
),
toArray()
)
).pipe(
map(([balance, [nominators = []]]: [ExtendedBalance, [Array<ExtendedBalance>]]): ExtendedBalance => {
const nominatedBalance = nominators.reduce((total, nominator: ExtendedBalance) => {
return total.add(nominator.votingBalance);
}, new BN(0));
return {
...balance,
nominators,
nominatedBalance,
stakingBalance: nominatedBalance.add(balance.votingBalance)
};
})
);
}
validatingBalances = (addresses: Array<string>): Observable<ExtendedBalanceMap> => {
return combineLatest(
addresses.map((address) =>
this.validatingBalance(address)
)
).pipe(
map((result: Array<ExtendedBalance>): ExtendedBalanceMap => {
return result.reduce((balances, balance) => {
balances[balance.address] = balance;
return balances;
}, {} as ExtendedBalanceMap);
})
);
}
votingBalance = (address: string): Observable<ExtendedBalance> => {
return combineLatest(
this.api.state.getStorage(storage.staking.public.freeBalanceOf, address),
this.api.state.getStorage(storage.staking.public.reservedBalanceOf, address)
).pipe(
map(([freeBalance = new BN(0), reservedBalance = new BN(0)]): ExtendedBalance => ({
address,
freeBalance,
nominatedBalance: new BN(0),
reservedBalance,
stakingBalance: new BN(0),
votingBalance: freeBalance.add(reservedBalance)
}))
);
}
votingBalances = (addresses: Array<string>): Observable<ExtendedBalance[]> => {
return combineLatest(
addresses.map((address) =>
this.votingBalance(address)
)
);
}
}

View File

@ -2,6 +2,8 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import BN from 'bn.js';
import { Observable } from 'rxjs';
import { ProviderInterface } from '@polkadot/api-provider/types';
import { RxApiInterface } from '@polkadot/api-rx/types';
import { EncodingVersions } from '@polkadot/params/types';
@ -12,9 +14,31 @@ export type BareProps = {
style?: { [index: string]: any }
};
export type ExtendedBalance = {
address: string,
freeBalance: BN,
nominatedBalance: BN,
reservedBalance: BN,
votingBalance: BN,
stakingBalance: BN,
nominators?: Array<ExtendedBalance>
}
export type ExtendedBalanceMap = {
[index: string]: ExtendedBalance
}
export interface ObservableApiInterface {
validatingBalance: (address: string) => Observable<ExtendedBalance>,
validatingBalances: (addresses: Array<string>) => Observable<ExtendedBalanceMap>,
votingBalance: (address: string) => Observable<ExtendedBalance>,
votingBalances: (addresses: Array<string>) => Observable<ExtendedBalance[]>
}
export type ApiProps = {
api: RxApiInterface,
apiConnected: boolean,
apiObservable: ObservableApiInterface,
apiSupport: EncodingVersions,
setApi: (api: RxApiInterface) => void,
setApiProvider: (provider?: ProviderInterface) => void,

View File

@ -0,0 +1,120 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// TODO: Lots of duplicated code between this and withObservable, surely there ois a better way of doing this?
import { RxProps, ObservableApiInterface } from '../types';
import { HOC, StorageOptions, DefaultProps } from './types';
import React from 'react';
import { map } from 'rxjs/operators/map';
import isUndefined from '@polkadot/util/is/undefined';
import intervalSubscribe from '../util/intervalSubscribe';
import isEqual from '../util/isEqual';
import triggerChange from '../util/triggerChange';
import echoTransform from './transform/echo';
import withApi from './api';
type State<T> = RxProps<T> & {
subscriptions: Array<any>; // FIXME subscriptions
};
type ObservableNames = keyof ObservableApiInterface;
// FIXME proper types for attributes
export default function withApiObservable<T> (observable: ObservableNames, { onChange, params, paramProp = 'params', propName = 'value', transform = echoTransform }: StorageOptions<T> = {}): HOC<T> {
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}): React.ComponentType<any> => {
class WithStorage extends React.Component<any, State<T>> {
state: State<T>;
constructor (props: any) {
super(props);
this.state = {
rxUpdated: false,
rxUpdatedAt: 0,
subscriptions: [],
value: void 0
};
}
componentDidUpdate (prevProps: any) {
if (!isEqual(this.props[paramProp], prevProps[paramProp])) {
this.subscribe();
}
}
componentDidMount () {
this.subscribe();
}
private subscribe () {
const { apiObservable } = this.props;
const propValue = params || this.props[paramProp];
this.unsubscribe();
if (isUndefined(propValue)) {
return;
}
this.setState({
subscriptions: [
apiObservable[observable](propValue)
.pipe(map(transform))
.subscribe((value: any) =>
this.triggerUpdate(this.props, value)
),
intervalSubscribe(this)
]
});
}
private unsubscribe () {
this.state.subscriptions.forEach((subscription) =>
subscription.unsubscribe()
);
}
componentWillUnmount () {
this.unsubscribe();
}
triggerUpdate = (props: any, value?: T): void => {
if (isEqual(value, this.state.value)) {
return;
}
triggerChange(value, onChange, props.onChange || defaultProps.onChange);
this.setState({
rxUpdated: true,
rxUpdatedAt: Date.now(),
value
});
}
render () {
const { rxUpdated, rxUpdatedAt, value } = this.state;
const _props = {
...defaultProps,
...this.props,
rxUpdated,
rxUpdatedAt,
[propName]: value
};
delete _props.onChange;
return (
<Inner {..._props} />
);
}
}
return withApi(WithStorage);
};
}

View File

@ -7,7 +7,11 @@ import React from 'react';
type HOC = (Component: React.ComponentType<any>) => React.ComponentType<any>;
export default function withMulti<T> (Component: React.ComponentType<any>, ...hocs: Array<HOC>): React.ComponentType<any> {
return hocs.reduce((Component, hoc) => {
return hoc(Component);
}, Component);
// NOTE: Order is reversed so it makes sense in the props, i.e. component
// after something can use the value of the preceding version
return hocs
.reverse()
.reduce((Component, hoc) => {
return hoc(Component);
}, Component);
}