285 lines
11 KiB
TypeScript
285 lines
11 KiB
TypeScript
import * as _ from 'lodash';
|
|
import Dialog from 'material-ui/Dialog';
|
|
import FlatButton from 'material-ui/FlatButton';
|
|
import * as moment from 'moment';
|
|
import * as React from 'react';
|
|
import firstBy from 'thenby';
|
|
|
|
import { Blockchain } from 'ts/blockchain';
|
|
import { NewTokenForm } from 'ts/components/generate_order/new_token_form';
|
|
import { TrackTokenConfirmation } from 'ts/components/track_token_confirmation';
|
|
import { TokenIcon } from 'ts/components/ui/token_icon';
|
|
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
|
|
import { Dispatcher } from 'ts/redux/dispatcher';
|
|
import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types';
|
|
import { constants } from 'ts/utils/constants';
|
|
import { utils } from 'ts/utils/utils';
|
|
|
|
const TOKEN_ICON_DIMENSION = 100;
|
|
const TILE_DIMENSION = 146;
|
|
enum AssetViews {
|
|
ASSET_PICKER = 'ASSET_PICKER',
|
|
NEW_TOKEN_FORM = 'NEW_TOKEN_FORM',
|
|
CONFIRM_TRACK_TOKEN = 'CONFIRM_TRACK_TOKEN',
|
|
}
|
|
|
|
interface AssetPickerProps {
|
|
userAddress: string;
|
|
blockchain: Blockchain;
|
|
dispatcher: Dispatcher;
|
|
networkId: number;
|
|
isOpen: boolean;
|
|
currentTokenAddress: string;
|
|
onTokenChosen: (tokenAddress: string) => void;
|
|
tokenByAddress: TokenByAddress;
|
|
tokenVisibility?: TokenVisibility;
|
|
}
|
|
|
|
interface AssetPickerState {
|
|
assetView: AssetViews;
|
|
hoveredAddress: string | undefined;
|
|
chosenTrackTokenAddress: string;
|
|
isAddingTokenToTracked: boolean;
|
|
}
|
|
|
|
export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerState> {
|
|
public static defaultProps: Partial<AssetPickerProps> = {
|
|
tokenVisibility: TokenVisibility.ALL,
|
|
};
|
|
private readonly _dialogConfigsByAssetView: { [assetView: string]: DialogConfigs };
|
|
constructor(props: AssetPickerProps) {
|
|
super(props);
|
|
this.state = {
|
|
assetView: AssetViews.ASSET_PICKER,
|
|
hoveredAddress: undefined,
|
|
chosenTrackTokenAddress: undefined,
|
|
isAddingTokenToTracked: false,
|
|
};
|
|
this._dialogConfigsByAssetView = {
|
|
[AssetViews.ASSET_PICKER]: {
|
|
title: 'Select token',
|
|
isModal: false,
|
|
actions: [],
|
|
},
|
|
[AssetViews.NEW_TOKEN_FORM]: {
|
|
title: 'Add an ERC20 token',
|
|
isModal: false,
|
|
actions: [],
|
|
},
|
|
[AssetViews.CONFIRM_TRACK_TOKEN]: {
|
|
title: 'Tracking confirmation',
|
|
isModal: true,
|
|
actions: [
|
|
<FlatButton
|
|
key="noTracking"
|
|
label="No"
|
|
onClick={this._onTrackConfirmationRespondedAsync.bind(this, false)}
|
|
/>,
|
|
<FlatButton
|
|
key="yesTrack"
|
|
label="Yes"
|
|
onClick={this._onTrackConfirmationRespondedAsync.bind(this, true)}
|
|
/>,
|
|
],
|
|
},
|
|
};
|
|
}
|
|
public render(): React.ReactNode {
|
|
const dialogConfigs: DialogConfigs = this._dialogConfigsByAssetView[this.state.assetView];
|
|
return (
|
|
<Dialog
|
|
title={dialogConfigs.title}
|
|
modal={dialogConfigs.isModal}
|
|
open={this.props.isOpen}
|
|
actions={dialogConfigs.actions}
|
|
autoScrollBodyContent={true}
|
|
onRequestClose={this._onCloseDialog.bind(this)}
|
|
>
|
|
{this.state.assetView === AssetViews.ASSET_PICKER && this._renderAssetPicker()}
|
|
{this.state.assetView === AssetViews.NEW_TOKEN_FORM && (
|
|
<NewTokenForm
|
|
blockchain={this.props.blockchain}
|
|
onNewTokenSubmitted={this._onNewTokenSubmitted.bind(this)}
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
/>
|
|
)}
|
|
{this.state.assetView === AssetViews.CONFIRM_TRACK_TOKEN && this._renderConfirmTrackToken()}
|
|
</Dialog>
|
|
);
|
|
}
|
|
private _renderConfirmTrackToken(): React.ReactNode {
|
|
const token = this.props.tokenByAddress[this.state.chosenTrackTokenAddress];
|
|
return (
|
|
<TrackTokenConfirmation
|
|
tokens={[token]}
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
networkId={this.props.networkId}
|
|
isAddingTokenToTracked={this.state.isAddingTokenToTracked}
|
|
/>
|
|
);
|
|
}
|
|
private _renderAssetPicker(): React.ReactNode {
|
|
return (
|
|
<div
|
|
className="flex flex-wrap"
|
|
style={{
|
|
maxWidth: 1000,
|
|
maxHeight: 600,
|
|
marginBottom: 10,
|
|
}}
|
|
>
|
|
{this._renderGridTiles()}
|
|
</div>
|
|
);
|
|
}
|
|
private _renderGridTiles(): React.ReactNode {
|
|
let isHovered;
|
|
let tileStyles;
|
|
const allTokens = _.values(this.props.tokenByAddress);
|
|
// filter tokens based on visibility specified in props, do not show ZRX or ETHER as tracked or untracked
|
|
const filteredTokens =
|
|
this.props.tokenVisibility === TokenVisibility.ALL
|
|
? allTokens
|
|
: _.filter(allTokens, token => {
|
|
return (
|
|
token.symbol !== constants.ZRX_TOKEN_SYMBOL &&
|
|
token.symbol !== constants.ETHER_TOKEN_SYMBOL &&
|
|
((this.props.tokenVisibility === TokenVisibility.TRACKED && utils.isTokenTracked(token)) ||
|
|
(this.props.tokenVisibility === TokenVisibility.UNTRACKED &&
|
|
!utils.isTokenTracked(token)))
|
|
);
|
|
});
|
|
// if we are showing tracked tokens, sort by date added, otherwise sort by symbol
|
|
const sortKey = this.props.tokenVisibility === TokenVisibility.TRACKED ? 'trackedTimestamp' : 'symbol';
|
|
const sortedTokens = filteredTokens.sort(firstBy(sortKey));
|
|
if (_.isEmpty(sortedTokens)) {
|
|
return <div className="mx-auto p4 h2">No tokens to remove.</div>;
|
|
}
|
|
const gridTiles = _.map(sortedTokens, token => {
|
|
const address = token.address;
|
|
isHovered = this.state.hoveredAddress === address;
|
|
tileStyles = {
|
|
cursor: 'pointer',
|
|
opacity: isHovered ? 0.6 : 1,
|
|
};
|
|
return (
|
|
<div
|
|
key={address}
|
|
style={{
|
|
width: TILE_DIMENSION,
|
|
height: TILE_DIMENSION,
|
|
...tileStyles,
|
|
}}
|
|
className="p2 flex sm-col-6 md-col-3 lg-col-3 flex-column items-center mx-auto"
|
|
onClick={this._onChooseToken.bind(this, address)}
|
|
onMouseEnter={this._onToggleHover.bind(this, address, true)}
|
|
onMouseLeave={this._onToggleHover.bind(this, address, false)}
|
|
>
|
|
<div className="p1">
|
|
<TokenIcon token={token} diameter={TOKEN_ICON_DIMENSION} />
|
|
</div>
|
|
<div className="center">{token.symbol}</div>
|
|
</div>
|
|
);
|
|
});
|
|
const otherTokenKey = 'otherToken';
|
|
isHovered = this.state.hoveredAddress === otherTokenKey;
|
|
tileStyles = {
|
|
cursor: 'pointer',
|
|
opacity: isHovered ? 0.6 : 1,
|
|
};
|
|
if (this.props.tokenVisibility !== TokenVisibility.TRACKED) {
|
|
gridTiles.push(
|
|
<div
|
|
key={otherTokenKey}
|
|
style={{
|
|
width: TILE_DIMENSION,
|
|
height: TILE_DIMENSION,
|
|
...tileStyles,
|
|
}}
|
|
className="p2 flex sm-col-6 md-col-3 lg-col-3 flex-column items-center mx-auto"
|
|
onClick={this._onCustomAssetChosen.bind(this)}
|
|
onMouseEnter={this._onToggleHover.bind(this, otherTokenKey, true)}
|
|
onMouseLeave={this._onToggleHover.bind(this, otherTokenKey, false)}
|
|
>
|
|
<div className="p1 center">
|
|
<i
|
|
style={{ fontSize: 105, paddingLeft: 1, paddingRight: 1 }}
|
|
className="zmdi zmdi-plus-circle"
|
|
/>
|
|
</div>
|
|
<div className="center">Other ERC20 Token</div>
|
|
</div>,
|
|
);
|
|
}
|
|
return gridTiles;
|
|
}
|
|
private _onToggleHover(address: string, isHovered: boolean): void {
|
|
const hoveredAddress = isHovered ? address : undefined;
|
|
this.setState({
|
|
hoveredAddress,
|
|
});
|
|
}
|
|
private _onCloseDialog(): void {
|
|
this.setState({
|
|
assetView: AssetViews.ASSET_PICKER,
|
|
});
|
|
this.props.onTokenChosen(this.props.currentTokenAddress);
|
|
}
|
|
private _onChooseToken(tokenAddress: string): void {
|
|
const token = this.props.tokenByAddress[tokenAddress];
|
|
if (utils.isTokenTracked(token)) {
|
|
this.props.onTokenChosen(tokenAddress);
|
|
} else {
|
|
this.setState({
|
|
assetView: AssetViews.CONFIRM_TRACK_TOKEN,
|
|
chosenTrackTokenAddress: tokenAddress,
|
|
});
|
|
}
|
|
}
|
|
private _onCustomAssetChosen(): void {
|
|
this.setState({
|
|
assetView: AssetViews.NEW_TOKEN_FORM,
|
|
});
|
|
}
|
|
private _onNewTokenSubmitted(newToken: Token): void {
|
|
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newToken);
|
|
this.props.dispatcher.addTokenToTokenByAddress(newToken);
|
|
this.setState({
|
|
assetView: AssetViews.ASSET_PICKER,
|
|
});
|
|
this.props.onTokenChosen(newToken.address);
|
|
}
|
|
private async _onTrackConfirmationRespondedAsync(didUserAcceptTracking: boolean): Promise<void> {
|
|
const resetState: AssetPickerState = {
|
|
...this.state,
|
|
isAddingTokenToTracked: false,
|
|
assetView: AssetViews.ASSET_PICKER,
|
|
chosenTrackTokenAddress: undefined,
|
|
};
|
|
if (!didUserAcceptTracking) {
|
|
this.setState(resetState);
|
|
this._onCloseDialog();
|
|
return;
|
|
}
|
|
this.setState({
|
|
isAddingTokenToTracked: true,
|
|
});
|
|
const tokenAddress = this.state.chosenTrackTokenAddress;
|
|
const token = this.props.tokenByAddress[tokenAddress];
|
|
if (_.isUndefined(tokenAddress)) {
|
|
this.setState(resetState);
|
|
return;
|
|
}
|
|
const newTokenEntry = {
|
|
...token,
|
|
trackedTimestamp: moment().unix(),
|
|
};
|
|
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
|
|
|
|
this.props.dispatcher.updateTokenByAddress([newTokenEntry]);
|
|
this.setState(resetState);
|
|
this.props.onTokenChosen(tokenAddress);
|
|
}
|
|
}
|