diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea76ee7..f7c80f4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## next release + +__added__ +- Coin visibility settings + ## 1.2.0-beta __added__ - Localization diff --git a/src/actions/LocalStorageActions.js b/src/actions/LocalStorageActions.js index 84fb9710..c44cdb33 100644 --- a/src/actions/LocalStorageActions.js +++ b/src/actions/LocalStorageActions.js @@ -12,7 +12,6 @@ import * as buildUtils from 'utils/build'; import * as storageUtils from 'utils/storage'; import * as WalletActions from 'actions/WalletActions'; import * as l10nUtils from 'utils/l10n'; - import { getAccountTokens } from 'reducers/utils'; import type { Account } from 'reducers/AccountsReducer'; import type { Token } from 'reducers/TokensReducer'; @@ -60,6 +59,8 @@ const KEY_BETA_MODAL: string = '/betaModalPrivacy'; // this key needs to be comp const KEY_LANGUAGE: string = `${STORAGE_PATH}language`; const KEY_LOCAL_CURRENCY: string = `${STORAGE_PATH}localCurrency`; const KEY_HIDE_BALANCE: string = `${STORAGE_PATH}hideBalance`; +const KEY_HIDDEN_COINS: string = `${STORAGE_PATH}hiddenCoins`; +const KEY_HIDDEN_COINS_EXTERNAL: string = `${STORAGE_PATH}hiddenCoinsExternal`; // https://github.com/STRML/react-localstorage/blob/master/react-localstorage.js // or @@ -247,6 +248,19 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } + const hiddenCoins = getHiddenCoins(false); + dispatch({ + type: WALLET.SET_HIDDEN_COINS, + hiddenCoins, + }); + + const isExternal = true; + const hiddenCoinsExternal = getHiddenCoins(isExternal); + dispatch({ + type: WALLET.SET_HIDDEN_COINS_EXTERNAL, + hiddenCoinsExternal, + }); + const userTokens: ?string = storageUtils.get(TYPE, KEY_TOKENS); if (userTokens) { dispatch({ @@ -350,6 +364,89 @@ export const getImportedAccounts = (): ?Array => { return null; }; +export const handleCoinVisibility = ( + coinShortcut: string, + shouldBeVisible: boolean, + isExternal: boolean +): ThunkAction => (dispatch: Dispatch): void => { + const configuration: Array = getHiddenCoins(isExternal); + let newConfig: Array = configuration; + const isAlreadyHidden = configuration.find(coin => coin === coinShortcut); + + if (shouldBeVisible) { + newConfig = configuration.filter(coin => coin !== coinShortcut); + } else if (!isAlreadyHidden) { + newConfig = [...configuration, coinShortcut]; + } + + if (isExternal) { + storageUtils.set(TYPE, KEY_HIDDEN_COINS_EXTERNAL, JSON.stringify(newConfig)); + dispatch({ + type: WALLET.SET_HIDDEN_COINS_EXTERNAL, + hiddenCoinsExternal: newConfig, + }); + } else { + storageUtils.set(TYPE, KEY_HIDDEN_COINS, JSON.stringify(newConfig)); + dispatch({ + type: WALLET.SET_HIDDEN_COINS, + hiddenCoins: newConfig, + }); + } +}; + +export const toggleGroupCoinsVisibility = ( + allCoins: Array, + checked: boolean, + isExternal: boolean +): ThunkAction => (dispatch: Dispatch) => { + // supported coins + if (checked && !isExternal) { + dispatch({ + type: WALLET.SET_HIDDEN_COINS, + hiddenCoins: [], + }); + storageUtils.set(TYPE, KEY_HIDDEN_COINS, JSON.stringify([])); + } + + if (!checked && !isExternal) { + dispatch({ + type: WALLET.SET_HIDDEN_COINS, + hiddenCoins: allCoins, + }); + storageUtils.set(TYPE, KEY_HIDDEN_COINS, JSON.stringify(allCoins)); + } + + // external coins + if (checked && isExternal) { + dispatch({ + type: WALLET.SET_HIDDEN_COINS_EXTERNAL, + hiddenCoinsExternal: [], + }); + storageUtils.set(TYPE, KEY_HIDDEN_COINS_EXTERNAL, JSON.stringify([])); + } + + if (!checked && isExternal) { + dispatch({ + type: WALLET.SET_HIDDEN_COINS_EXTERNAL, + hiddenCoinsExternal: allCoins, + }); + storageUtils.set(TYPE, KEY_HIDDEN_COINS_EXTERNAL, JSON.stringify(allCoins)); + } +}; + +export const getHiddenCoins = (isExternal: boolean): Array => { + let coinsConfig: ?string = ''; + if (isExternal) { + coinsConfig = storageUtils.get(TYPE, KEY_HIDDEN_COINS_EXTERNAL); + } else { + coinsConfig = storageUtils.get(TYPE, KEY_HIDDEN_COINS); + } + if (coinsConfig) { + return JSON.parse(coinsConfig); + } + return []; +}; + export const removeImportedAccounts = (device: TrezorDevice): ThunkAction => ( dispatch: Dispatch ): void => { diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index 1aa9a853..ed84b5e8 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -25,6 +25,14 @@ export type WalletAction = state?: RouterLocationState, pathname?: string, } + | { + type: typeof WALLET.SET_HIDDEN_COINS, + hiddenCoins: Array, + } + | { + type: typeof WALLET.SET_HIDDEN_COINS_EXTERNAL, + hiddenCoinsExternal: Array, + } | { type: typeof WALLET.TOGGLE_DEVICE_DROPDOWN, opened: boolean, diff --git a/src/actions/constants/wallet.js b/src/actions/constants/wallet.js index fa3bf052..562cd539 100644 --- a/src/actions/constants/wallet.js +++ b/src/actions/constants/wallet.js @@ -19,3 +19,6 @@ export const TOGGLE_SIDEBAR: 'wallet__toggle_sidebar' = 'wallet__toggle_sidebar' export const SET_LANGUAGE: 'wallet__set_language' = 'wallet__set_language'; export const SET_LOCAL_CURRENCY: 'wallet__set_local_currency' = 'wallet__set_local_currency'; export const SET_HIDE_BALANCE: 'wallet__set_hide_balance' = 'wallet__set_hide_balance'; +export const SET_HIDDEN_COINS: 'wallet__set_hidden_coins' = 'wallet__set_hidden_coins'; +export const SET_HIDDEN_COINS_EXTERNAL: 'wallet__set_hidden_coins_external' = + 'wallet__set_hidden_coins_external'; diff --git a/src/reducers/WalletReducer.js b/src/reducers/WalletReducer.js index fab169a2..04d3a15e 100644 --- a/src/reducers/WalletReducer.js +++ b/src/reducers/WalletReducer.js @@ -24,6 +24,8 @@ type State = { firstLocationChange: boolean, disconnectRequest: ?TrezorDevice, selectedDevice: ?TrezorDevice, + hiddenCoins: Array, + hiddenCoinsExternal: Array, }; const initialState: State = { @@ -41,6 +43,8 @@ const initialState: State = { initialPathname: null, disconnectRequest: null, selectedDevice: null, + hiddenCoins: [], + hiddenCoinsExternal: [], }; export default function wallet(state: State = initialState, action: Action): State { @@ -145,6 +149,18 @@ export default function wallet(state: State = initialState, action: Action): Sta hideBalance: action.toggled, }; + case WALLET.SET_HIDDEN_COINS: + return { + ...state, + hiddenCoins: action.hiddenCoins, + }; + + case WALLET.SET_HIDDEN_COINS_EXTERNAL: + return { + ...state, + hiddenCoinsExternal: action.hiddenCoinsExternal, + }; + default: return state; } diff --git a/src/utils/__tests__/l10n.js b/src/utils/__tests__/l10n.test.js similarity index 100% rename from src/utils/__tests__/l10n.js rename to src/utils/__tests__/l10n.test.js diff --git a/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js b/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js index f3ee7fda..c1e1ce2b 100644 --- a/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js @@ -26,6 +26,21 @@ const StyledLink = styled(Link)` } `; +const Empty = styled.span` + display: flex; + justify-content: center; + align-items: center; + min-height: 50px; +`; + +const StyledLinkEmpty = styled(Link)` + padding: 0; +`; + +const Gray = styled.span` + color: ${colors.TEXT_SECONDARY}; +`; + class CoinMenu extends PureComponent { getBaseUrl() { const { selectedDevice } = this.props.wallet; @@ -41,8 +56,11 @@ class CoinMenu extends PureComponent { } getOtherCoins() { + const { hiddenCoinsExternal } = this.props.wallet; return coins .sort((a, b) => a.order - b.order) + .filter(item => !item.isHidden) // hide coins globally in config + .filter(item => !hiddenCoinsExternal.includes(item.id)) .map(coin => { const row = ( { }); } + isTopMenuEmpty() { + const numberOfVisibleNetworks = this.props.localStorage.config.networks + .filter(item => !item.isHidden) // hide coins globally in config + .filter(item => !this.props.wallet.hiddenCoins.includes(item.shortcut)); + + return numberOfVisibleNetworks.length <= 0; + } + + isBottomMenuEmpty() { + const { hiddenCoinsExternal } = this.props.wallet; + const numberOfVisibleNetworks = coins + .filter(item => !item.isHidden) + .filter(item => !hiddenCoinsExternal.includes(item.id)); + + return numberOfVisibleNetworks.length <= 0; + } + + isMenuEmpty() { + return this.isTopMenuEmpty() && this.isBottomMenuEmpty(); + } + render() { + const { hiddenCoins } = this.props.wallet; const { config } = this.props.localStorage; return ( + {this.isMenuEmpty() && ( + + + + + + ), + }} + />{' '} + + + )} {config.networks - .filter(item => !item.isHidden) + .filter(item => !item.isHidden) // hide coins globally in config + .filter(item => !hiddenCoins.includes(item.shortcut)) // hide coins by user settings .sort((a, b) => a.order - b.order) .map(item => ( { /> ))} - } - hasBorder - /> + {!this.isBottomMenuEmpty() && ( + } + hasBorder + /> + )} {this.getOtherCoins()} ); diff --git a/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.messages.js b/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.messages.js index 78b43096..fa837418 100644 --- a/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.messages.js +++ b/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.messages.js @@ -7,6 +7,15 @@ const definedMessages: Messages = defineMessages({ id: 'TR_OTHER_COINS', defaultMessage: 'Other coins', }, + TR_SELECT_COINS: { + id: 'TR_SELECT_COINS', + description: 'COMPLETE SENTENCE: Select a coin in application settings', + defaultMessage: 'Select a coin in {TR_SELECT_COINS_LINK}', + }, + TR_SELECT_COINS_LINK: { + id: 'TR_SELECT_COINS_LINK', + defaultMessage: 'application settings', + }, }); export default definedMessages; diff --git a/src/views/Wallet/views/Dashboard/index.js b/src/views/Wallet/views/Dashboard/index.js index 4551adbc..273dfe88 100644 --- a/src/views/Wallet/views/Dashboard/index.js +++ b/src/views/Wallet/views/Dashboard/index.js @@ -80,6 +80,7 @@ const Dashboard = (props: Props) => ( {props.localStorage.config.networks .filter(item => !item.isHidden) + .filter(item => !props.wallet.hiddenCoins.includes(item.shortcut)) .map(network => ( ({ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ setLocalCurrency: bindActionCreators(WalletActions.setLocalCurrency, dispatch), setHideBalance: bindActionCreators(WalletActions.setHideBalance, dispatch), + handleCoinVisibility: bindActionCreators(LocalStorageActions.handleCoinVisibility, dispatch), + toggleGroupCoinsVisibility: bindActionCreators( + LocalStorageActions.toggleGroupCoinsVisibility, + dispatch + ), }); export default injectIntl( diff --git a/src/views/Wallet/views/WalletSettings/components/Coins/index.js b/src/views/Wallet/views/WalletSettings/components/Coins/index.js new file mode 100644 index 00000000..21aab25a --- /dev/null +++ b/src/views/Wallet/views/WalletSettings/components/Coins/index.js @@ -0,0 +1,261 @@ +/* @flow */ +import styled from 'styled-components'; +import React, { PureComponent } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { FONT_SIZE } from 'config/variables'; +import coins from 'constants/coins'; +import * as LocalStorageActions from 'actions/LocalStorageActions'; +import type { Network } from 'flowtype'; + +import { colors, Switch, CoinLogo, Tooltip, Icon, icons as ICONS } from 'trezor-ui-components'; +import l10nMessages from '../../index.messages'; + +type Props = { + networks: Array, + hiddenCoins: Array, + hiddenCoinsExternal: Array, + handleCoinVisibility: typeof LocalStorageActions.handleCoinVisibility, + toggleGroupCoinsVisibility: typeof LocalStorageActions.toggleGroupCoinsVisibility, +}; + +type State = { + showAllCoins: boolean, + showAllCoinsExternal: boolean, +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; +`; + +const Row = styled.div` + display: flex; + flex-direction: column; +`; + +const Content = styled.div` + display: flex; + margin: 20px 0; + flex-direction: column; +`; + +const Label = styled.div` + display: flex; + padding: 10px 0; + justify-content: space-between; + color: ${colors.TEXT_SECONDARY}; + align-items: center; +`; + +const TooltipIcon = styled(Icon)` + margin-left: 6px; + cursor: pointer; +`; + +const CoinRow = styled.div` + height: 50px; + align-items: center; + display: flex; + border-bottom: 1px solid ${colors.DIVIDER}; + color: ${colors.TEXT_PRIMARY}; + justify-content: space-between; + + &:first-child { + border-top: 1px solid ${colors.DIVIDER}; + } + + &:last-child { + border-bottom: 0; + } +`; + +const Left = styled.div` + display: flex; + align-items: center; +`; + +const Right = styled.div` + display: flex; + align-items: center; +`; + +const Name = styled.div` + display: flex; + font-size: ${FONT_SIZE.BIG}; + color: ${colors.TEXT_PRIMARY}; +`; + +const LogoWrapper = styled.div` + display: flex; + width: 45px; + justify-content: center; + align-items: center; +`; + +const ToggleAll = styled.div` + cursor: pointer; +`; + +class CoinsSettings extends PureComponent { + constructor(props: Props) { + super(props); + this.state = { + showAllCoins: this.props.hiddenCoins.length === 0, + showAllCoinsExternal: this.props.hiddenCoinsExternal.length === 0, + }; + } + + render() { + const { props } = this; + return ( + + + + + {props.networks + .filter(network => !network.isHidden) + .map(network => ( + + + + + + {network.name} + + + { + props.handleCoinVisibility( + network.shortcut, + visible, + false + ); + }} + checked={!props.hiddenCoins.includes(network.shortcut)} + /> + + + ))} + + + + {coins + .sort((a, b) => a.order - b.order) + .map(network => ( + + + + + + {network.coinName} + + + { + props.handleCoinVisibility( + network.id, + visible, + true + ); + }} + checked={ + !props.hiddenCoinsExternal.includes(network.id) + } + /> + + + ))} + + + + ); + } +} + +export default CoinsSettings; diff --git a/src/views/Wallet/views/WalletSettings/index.js b/src/views/Wallet/views/WalletSettings/index.js index e8c4a752..a9b4626b 100644 --- a/src/views/Wallet/views/WalletSettings/index.js +++ b/src/views/Wallet/views/WalletSettings/index.js @@ -17,6 +17,7 @@ import { import { FIAT_CURRENCIES } from 'config/app'; import { FONT_SIZE } from 'config/variables'; import l10nCommonMessages from 'views/common.messages'; +import Coins from './components/Coins'; import l10nMessages from './index.messages'; import type { Props } from './Container'; @@ -72,7 +73,10 @@ const TooltipIcon = styled(Icon)` `; const buildCurrencyOption = currency => { - return { value: currency, label: currency.toUpperCase() }; + return { + value: currency, + label: currency.toUpperCase(), + }; }; const WalletSettings = (props: Props) => ( @@ -102,6 +106,9 @@ const WalletSettings = (props: Props) => ( { props.setHideBalance(checked); }} @@ -109,6 +116,15 @@ const WalletSettings = (props: Props) => ( /> +
+ +
diff --git a/src/views/Wallet/views/WalletSettings/index.messages.js b/src/views/Wallet/views/WalletSettings/index.messages.js index 43557498..b22ad616 100644 --- a/src/views/Wallet/views/WalletSettings/index.messages.js +++ b/src/views/Wallet/views/WalletSettings/index.messages.js @@ -16,6 +16,19 @@ const definedMessages: Messages = defineMessages({ id: 'TR_THE_CHANGES_ARE_SAVED', defaultMessage: 'The changes are saved automatically as they are made', }, + TR_VISIBLE_COINS: { + id: 'TR_VISIBLE_COINS', + defaultMessage: 'Visible coins', + }, + TR_VISIBLE_COINS_EXTERNAL: { + id: 'TR_VISIBLE_COINS', + defaultMessage: 'Visible external coins', + }, + TR_VISIBLE_COINS_EXPLAINED: { + id: 'TR_VISIBLE_COINS_EXPLAINED', + defaultMessage: + 'Select the coins you wish to see in the wallet interface. You will be able to change your preferences later.', + }, }); export default definedMessages;