diff --git a/packages/app-staking/src/Account.tsx b/packages/app-staking/src/Account.tsx index 4a1a885fcb..17f807a7d0 100644 --- a/packages/app-staking/src/Account.tsx +++ b/packages/app-staking/src/Account.tsx @@ -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, intentions: Array, isValidator: boolean, @@ -52,7 +54,7 @@ class Account extends React.PureComponent { } 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 { intentions={intentions} /> @@ -77,17 +80,58 @@ class Account extends React.PureComponent { size='large' /> {this.renderButtons()} - + {this.renderNominee()} + {this.renderNominators()} ); } + private balanceArray (address: string): Array | 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 ( + + ); + } + + private renderNominators () { + const { nominatorsFor } = this.props; + + if (!nominatorsFor) { + return null; + } + + return nominatorsFor.map((nominator) => ( + + )); + } + 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 { @@ -199,7 +243,7 @@ export default withMulti( withStorage( storage.staking.public.nominating, { - propName: 'nominating', + propName: 'nominee', paramProp: 'address', transform: encodeAddress } diff --git a/packages/app-staking/src/StakeList.tsx b/packages/app-staking/src/StakeList.tsx index 4880d0e49f..230a669c20 100644 --- a/packages/app-staking/src/StakeList.tsx +++ b/packages/app-staking/src/StakeList.tsx @@ -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, validators: Array }; class StakeList extends React.PureComponent { render () { - const { className, intentions, style, validators } = this.props; + const { balances, className, intentions, style, validators } = this.props; return ( @@ -36,6 +38,7 @@ class StakeList extends React.PureComponent { return ( , lastBlockHeader?: Header, lastLengthChange?: BN, @@ -28,60 +27,11 @@ type Props = ApiProps & I18nProps & { validators: Array }; -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 { - 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 { 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 { ); } - 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 { }, 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))) { diff --git a/packages/app-staking/src/index.tsx b/packages/app-staking/src/index.tsx index 9531f3459f..2c5c00793c 100644 --- a/packages/app-staking/src/index.tsx +++ b/packages/app-staking/src/index.tsx @@ -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, validators?: Array }; @@ -26,7 +29,7 @@ const transformAddresses = (publicKeys: Array) => class App extends React.PureComponent { render () { - const { className, intentions = [], style, validators = [] } = this.props; + const { balances = {}, className, intentions = [], style, validators = [] } = this.props; return (
{ style={style} > @@ -61,5 +66,12 @@ export default withMulti( propName: 'validators', transform: transformAddresses } + ), + withApiObservable( + 'validatingBalances', + { + paramProp: 'intentions', + propName: 'balances' + } ) ); diff --git a/packages/app-staking/src/observables/types.d.ts b/packages/app-staking/src/observables/types.d.ts deleted file mode 100644 index f2e39aa7c7..0000000000 --- a/packages/app-staking/src/observables/types.d.ts +++ /dev/null @@ -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 -} - -export type BalanceMap = { - [index: string]: Balance -} diff --git a/packages/app-staking/src/observables/validatingBalance.ts b/packages/app-staking/src/observables/validatingBalance.ts deleted file mode 100644 index 0ac8d61f14..0000000000 --- a/packages/app-staking/src/observables/validatingBalance.ts +++ /dev/null @@ -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]]; - -export default function validatingBalance (api: RxApiInterface, address: string): Observable { - 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) - }; - }) - ); -} diff --git a/packages/app-staking/src/observables/validatingBalances.ts b/packages/app-staking/src/observables/validatingBalances.ts deleted file mode 100644 index 850f923095..0000000000 --- a/packages/app-staking/src/observables/validatingBalances.ts +++ /dev/null @@ -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): Observable { - return combineLatest( - addresses.map((address) => - validatingBalance(api, address) - ) - ).pipe( - map((result: Array): BalanceMap => { - return result.reduce((balances, balance) => { - balances[balance.address] = balance; - - return balances; - }, {} as BalanceMap); - }) - ); -} diff --git a/packages/app-staking/src/observables/votingBalance.ts b/packages/app-staking/src/observables/votingBalance.ts deleted file mode 100644 index 8a4a1bd40d..0000000000 --- a/packages/app-staking/src/observables/votingBalance.ts +++ /dev/null @@ -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 { - 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) - })) - ); -} diff --git a/packages/app-staking/src/observables/votingBalances.ts b/packages/app-staking/src/observables/votingBalances.ts deleted file mode 100644 index 0ea248d9e0..0000000000 --- a/packages/app-staking/src/observables/votingBalances.ts +++ /dev/null @@ -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): Observable { - return combineLatest( - addresses.map((address) => - votingBalance(api, address) - ) - ); -} diff --git a/packages/ui-app/src/AddressMini.tsx b/packages/ui-app/src/AddressMini.tsx index 56d7d3a13f..9195608fe3 100644 --- a/packages/ui-app/src/AddressMini.tsx +++ b/packages/ui-app/src/AddressMini.tsx @@ -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, isShort?: boolean, - value?: string + value?: string, + withBalance?: boolean }; -class AddressMini extends React.PureComponent { +export default class AddressMini extends React.PureComponent { render () { const { className, isShort = true, style, value } = this.props; @@ -24,21 +27,36 @@ class AddressMini extends React.PureComponent { return null; } - const shortValue = toShortAddress(value); - return (
- -
{isShort ? shortValue : value}
+
+ +
{isShort ? toShortAddress(value) : value}
+
+ {this.renderBalance()}
); } -} -export default translate(AddressMini); + private renderBalance () { + const { balance, value, withBalance = false } = this.props; + + if (!withBalance || !value) { + return null; + } + + return ( + + ); + } +} diff --git a/packages/ui-app/src/AddressRow.tsx b/packages/ui-app/src/AddressRow.tsx index 488ab92a65..de69538c08 100644 --- a/packages/ui-app/src/AddressRow.tsx +++ b/packages/ui-app/src/AddressRow.tsx @@ -29,6 +29,7 @@ class AddressRow extends AddressSummary {
{this.renderAddress()} {this.renderBalance()} + {this.renderNonce()}
{this.renderChildren()} diff --git a/packages/ui-app/src/AddressSummary.tsx b/packages/ui-app/src/AddressSummary.tsx index 82d6fc821e..d357c8efc7 100644 --- a/packages/ui-app/src/AddressSummary.tsx +++ b/packages/ui-app/src/AddressSummary.tsx @@ -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, children?: React.ReactNode, name?: string, value: string, @@ -74,6 +75,7 @@ class AddressSummary extends React.PureComponent { /> {this.renderAddress()} {this.renderBalance()} + {this.renderNonce()} {this.renderChildren()} @@ -99,40 +101,42 @@ class AddressSummary extends React.PureComponent { 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 ( + + ); + } - return [ - withBalance - ? ( - - ) - : null, - withNonce - ? ( - - {t('addressSummary.transactions', { - defaultValue: ' transactions' - })} - - ) - : null - ]; + protected renderNonce () { + const { isValid, publicKey } = this.state; + const { t, withNonce = true } = this.props; + + if (!withNonce || !isValid) { + return null; + } + + return ( + + {t('addressSummary.transactions', { + defaultValue: ' transactions' + })} + + ); } protected renderChildren () { diff --git a/packages/ui-app/src/Balance.tsx b/packages/ui-app/src/Balance.tsx new file mode 100644 index 0000000000..cb6df21824 --- /dev/null +++ b/packages/ui-app/src/Balance.tsx @@ -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, + label?: string, + value: string | Uint8Array, + withLabel?: boolean +}; + +class Balance extends React.PureComponent { + render () { + const { balance, className, label, value, style } = this.props; + + return balance + ? this.renderProvided() + : ( + + ); + } + + 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 ( +
+ {label}{value} +
+ ); + } +} + +export default translate(Balance); diff --git a/packages/ui-app/src/styles/components.css b/packages/ui-app/src/styles/components.css index 3795a55445..dfabb2450a 100644 --- a/packages/ui-app/src/styles/components.css +++ b/packages/ui-app/src/styles/components.css @@ -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; diff --git a/packages/ui-react-rx/src/Api/index.tsx b/packages/ui-react-rx/src/Api/index.tsx index afd6c72745..5d87ba7c39 100644 --- a/packages/ui-react-rx/src/Api/index.tsx +++ b/packages/ui-react-rx/src/Api/index.tsx @@ -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 { : 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 { 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 { } render () { - const { api, apiConnected, apiSupport, setApi, setApiProvider, setApiWsUrl } = this.state; + const { api, apiConnected, apiObservable, apiSupport, setApi, setApiProvider, setApiWsUrl } = this.state; return ( => { + 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 => { + 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): Observable => { + return combineLatest( + addresses.map((address) => + this.validatingBalance(address) + ) + ).pipe( + map((result: Array): ExtendedBalanceMap => { + return result.reduce((balances, balance) => { + balances[balance.address] = balance; + + return balances; + }, {} as ExtendedBalanceMap); + }) + ); + } + + votingBalance = (address: string): Observable => { + 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): Observable => { + return combineLatest( + addresses.map((address) => + this.votingBalance(address) + ) + ); + } +} diff --git a/packages/ui-react-rx/src/types.d.ts b/packages/ui-react-rx/src/types.d.ts index cefe2e5ed8..c2dc222dae 100644 --- a/packages/ui-react-rx/src/types.d.ts +++ b/packages/ui-react-rx/src/types.d.ts @@ -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 +} + +export type ExtendedBalanceMap = { + [index: string]: ExtendedBalance +} + +export interface ObservableApiInterface { + validatingBalance: (address: string) => Observable, + validatingBalances: (addresses: Array) => Observable, + votingBalance: (address: string) => Observable, + votingBalances: (addresses: Array) => Observable +} + export type ApiProps = { api: RxApiInterface, apiConnected: boolean, + apiObservable: ObservableApiInterface, apiSupport: EncodingVersions, setApi: (api: RxApiInterface) => void, setApiProvider: (provider?: ProviderInterface) => void, diff --git a/packages/ui-react-rx/src/with/apiObservable.tsx b/packages/ui-react-rx/src/with/apiObservable.tsx new file mode 100644 index 0000000000..aa412c57f8 --- /dev/null +++ b/packages/ui-react-rx/src/with/apiObservable.tsx @@ -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 = RxProps & { + subscriptions: Array; // FIXME subscriptions +}; + +type ObservableNames = keyof ObservableApiInterface; + +// FIXME proper types for attributes + +export default function withApiObservable (observable: ObservableNames, { onChange, params, paramProp = 'params', propName = 'value', transform = echoTransform }: StorageOptions = {}): HOC { + return (Inner: React.ComponentType, defaultProps: DefaultProps = {}): React.ComponentType => { + class WithStorage extends React.Component> { + state: State; + + 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 ( + + ); + } + } + + return withApi(WithStorage); + }; +} diff --git a/packages/ui-react-rx/src/with/multi.ts b/packages/ui-react-rx/src/with/multi.ts index 7031f40677..ac17f6b763 100644 --- a/packages/ui-react-rx/src/with/multi.ts +++ b/packages/ui-react-rx/src/with/multi.ts @@ -7,7 +7,11 @@ import React from 'react'; type HOC = (Component: React.ComponentType) => React.ComponentType; export default function withMulti (Component: React.ComponentType, ...hocs: Array): React.ComponentType { - 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); }