diff --git a/CHANGELOG.md b/CHANGELOG.md index 68deb0053d..5cd122b68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## vNext + +### Features +- Improved stake pool searchbar ([PR 2847](https://github.com/input-output-hk/daedalus/pull/2847)) + ## 4.9.0-FC1 ### Features diff --git a/source/common/config/electron-store.config.js b/source/common/config/electron-store.config.js index e622735195..969f61952a 100644 --- a/source/common/config/electron-store.config.js +++ b/source/common/config/electron-store.config.js @@ -26,6 +26,7 @@ export const STORAGE_KEYS: { READ_NEWS: 'READ-NEWS', RESET: 'RESET', SMASH_SERVER: 'SMASH-SERVER', + STAKE_POOLS_LIST_VIEW_TOOLTIP: 'STAKE-POOLS-LIST-VIEW-TOOLTIP', STAKING_INFO_WAS_OPEN: 'ALONZO-INFO-WAS-OPEN', TERMS_OF_USE_ACCEPTANCE: 'TERMS-OF-USE-ACCEPTANCE', THEME: 'THEME', diff --git a/source/common/types/electron-store.types.js b/source/common/types/electron-store.types.js index f0ef4e9625..881c15739c 100644 --- a/source/common/types/electron-store.types.js +++ b/source/common/types/electron-store.types.js @@ -18,6 +18,7 @@ export type StorageKey = | 'READ-NEWS' | 'RESET' | 'SMASH-SERVER' + | 'STAKE-POOLS-LIST-VIEW-TOOLTIP' | 'TERMS-OF-USE-ACCEPTANCE' | 'THEME' | 'TOKEN-FAVORITES' diff --git a/source/main/ipc/electronStoreConversation.js b/source/main/ipc/electronStoreConversation.js index 3fea245e23..a3c2b2d8f2 100644 --- a/source/main/ipc/electronStoreConversation.js +++ b/source/main/ipc/electronStoreConversation.js @@ -39,6 +39,7 @@ const reset = async () => { await unset(keys.READ_NEWS); await unset(keys.SMASH_SERVER); await unset(keys.STAKING_INFO_WAS_OPEN); + await unset(keys.STAKE_POOLS_LIST_VIEW_TOOLTIP); await unset(keys.TERMS_OF_USE_ACCEPTANCE); await unset(keys.THEME); await unset(keys.USER_DATE_FORMAT_ENGLISH); diff --git a/source/renderer/app/api/utils/localStorage.js b/source/renderer/app/api/utils/localStorage.js index 78e3206185..e142c0cdb4 100644 --- a/source/renderer/app/api/utils/localStorage.js +++ b/source/renderer/app/api/utils/localStorage.js @@ -478,7 +478,12 @@ export default class LocalStorageApi { unsetHardwareWalletDevicesAll = async (): Promise => LocalStorageApi.unset(keys.HARDWARE_WALLET_DEVICES); - + setStakePoolsListViewTooltip = async (visited: boolean): Promise => + LocalStorageApi.set(keys.STAKE_POOLS_LIST_VIEW_TOOLTIP, visited); + getStakePoolsListViewTooltip = async (): Promise => + LocalStorageApi.get(keys.STAKE_POOLS_LIST_VIEW_TOOLTIP, true); + unsetStakePoolsListViewTooltip = async (): Promise => + LocalStorageApi.unset(keys.STAKE_POOLS_LIST_VIEW_TOOLTIP); reset = async () => { await LocalStorageApi.reset(); }; diff --git a/source/renderer/app/components/staking/stake-pools/StakePools.js b/source/renderer/app/components/staking/stake-pools/StakePools.js index 134f0f5c0f..5a4a103b5a 100644 --- a/source/renderer/app/components/staking/stake-pools/StakePools.js +++ b/source/renderer/app/components/staking/stake-pools/StakePools.js @@ -71,24 +71,26 @@ const messages = defineMessages({ const SELECTED_INDEX_TABLE = 'selectedIndexTable'; type Props = { - wallets: Array, - currentLocale: string, - stakePoolsList: Array, - onOpenExternalLink: Function, - currentTheme: string, - updateDelegatingStake: Function, - rankStakePools: Function, - selectedDelegationWalletId?: ?string, - stake?: ?number, - onDelegate: Function, - isLoading: boolean, - isFetching: boolean, - isRanking: boolean, - stakePoolsDelegatingList: Array, - getStakePoolById: Function, - onSmashSettingsClick: Function, - smashServerUrl: ?string, - maxDelegationFunds: number, + currentLocale: string; + currentTheme: string; + getStakePoolById: (...args: Array) => any; + isFetching: boolean; + isListViewTooltipVisible?: boolean; + isLoading: boolean; + isRanking: boolean; + maxDelegationFunds: number; + onDelegate: (...args: Array) => any; + onListViewVisited: () => void; + onOpenExternalLink: (...args: Array) => any; + onSmashSettingsClick: (...args: Array) => any; + rankStakePools: (...args: Array) => any; + selectedDelegationWalletId?: string | null | undefined; + smashServerUrl: string | null | undefined; + stake?: number | null | undefined; + stakePoolsDelegatingList: Array; + stakePoolsList: Array; + updateDelegatingStake: (...args: Array) => any; + wallets: Array; }; type State = { @@ -137,14 +139,13 @@ export default class StakePools extends Component { isListView: false, }); }; - - handleListView = () => + handleListView = () => { this.setState({ isGridView: false, isGridRewardsView: false, isListView: true, }); - + }; handleSetListActive = (selectedList: string) => this.setState({ selectedList }); @@ -170,9 +171,11 @@ export default class StakePools extends Component { selectedDelegationWalletId, stake, onOpenExternalLink, + onListViewVisited, currentTheme, - isLoading, isFetching, + isListViewTooltipVisible, + isLoading, isRanking, stakePoolsDelegatingList, getStakePoolById, @@ -278,7 +281,9 @@ export default class StakePools extends Component { onGridView={this.handleGridView} onGridRewardsView={this.handleGridRewardsView} onListView={this.handleListView} + onListViewVisited={onListViewVisited} isListView={isListView} + isListViewTooltipVisible={isListViewTooltipVisible} isGridView={isGridView} isGridRewardsView={isGridRewardsView} smashServer={smashServer} diff --git a/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.js b/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.js index 2a2592bcd5..18f55e4472 100644 --- a/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.js +++ b/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.js @@ -1,7 +1,6 @@ -// @flow -import React, { Component } from 'react'; +import React, { useRef } from 'react'; import SVGInline from 'react-svg-inline'; -import { defineMessages, intlShape } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { Input } from 'react-polymorph/lib/components/Input'; import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import { PopOver } from 'react-polymorph/lib/components/PopOver'; @@ -11,190 +10,145 @@ import searchIcon from '../../../assets/images/search.inline.svg'; import closeIcon from '../../../assets/images/close-cross.inline.svg'; import gridIcon from '../../../assets/images/grid-ic.inline.svg'; import gridRewardsIcon from '../../../assets/images/grid-rewards.inline.svg'; -import listIcon from '../../../assets/images/list-ic.inline.svg'; import { IS_GRID_REWARDS_VIEW_AVAILABLE } from '../../../config/stakingConfig'; - -const messages = defineMessages({ - searchInputPlaceholder: { - id: 'staking.stakePools.search.searchInputPlaceholder', - defaultMessage: '!!!Search stake pools', - description: '"Delegating List Title" for the Stake Pools search.', - }, - delegatingListTitle: { - id: 'staking.stakePools.search.delegatingListTitle', - defaultMessage: '!!!Stake pools to which you are delegating', - description: '"delegatingListTitle" for the Stake Pools search.', - }, - listTitle: { - id: 'staking.stakePools.search.listTitle', - defaultMessage: '!!!Stake pools ({pools})', - description: '"listTitle" for the Stake Pools search.', - }, - gridIconTooltip: { - id: 'staking.stakePools.search.gridIconTooltip', - defaultMessage: '!!!Grid View', - description: '"gridIconTooltip" for the Stake Pools search.', - }, - gridRewardsIconTooltip: { - id: 'staking.stakePools.search.gridRewardsIconTooltip', - defaultMessage: '!!!Grid Rewards View', - description: '"gridRewardsIconTooltip" for the Stake Pools search.', - }, - listIconTooltip: { - id: 'staking.stakePools.search.listIconTooltip', - defaultMessage: '!!!List View', - description: '"listIconTooltip" for the Stake Pools search.', - }, - clearTooltip: { - id: 'staking.stakePools.search.clearTooltip', - defaultMessage: '!!!Clear', - description: '"clearTooltip" for the Stake Pools search.', - }, -}); +import type { Intl } from '../../../types/i18nTypes'; +import { messages } from './StakePoolsSearch.messages'; +import { StakePoolsSearchListViewButton } from './StakePoolsSearchListViewButton'; type Props = { - label?: string, - placeholder?: string, - isListView?: boolean, - isGridView?: boolean, - isGridRewardsView?: boolean, - onSearch: Function, - onClearSearch: Function, - onGridView?: Function, - onGridRewardsView?: Function, - onListView?: Function, - search: string, + label?: string; + placeholder?: string; + isListView?: boolean; + isListViewTooltipVisible?: boolean; + isGridView?: boolean; + isGridRewardsView?: boolean; + onSearch: (...args: Array) => any; + onClearSearch: (...args: Array) => any; + onGridView?: (...args: Array) => any; + onGridRewardsView?: (...args: Array) => any; + onListView?: (...args: Array) => any; + onListViewVisited?: () => void; + search: string; + intl: Intl; }; -export class StakePoolsSearch extends Component { - static contextTypes = { - intl: intlShape.isRequired, - }; - - searchInput: ?Object = null; +function StakePoolsSearchComponent({ + label, + onClearSearch, + onSearch, + onGridView, + onGridRewardsView, + onListView, + onListViewVisited, + placeholder, + search, + isListView, + isListViewTooltipVisible, + isGridView, + isGridRewardsView, + intl, +}: Props) { + const searchInput = useRef<{ inputElement: { current: HTMLInputElement } }>( + null + ); - autoSelectOnFocus = () => - this.searchInput ? this.searchInput.inputElement.current.select() : false; + const autoSelectOnFocus = () => + searchInput?.current + ? searchInput?.current?.inputElement.current.select() + : false; - get hasSearchClearButton() { - return this.props.search.length > 0; - } + const handleClearSearch = () => { + onClearSearch(); - handleClearSearch = () => { - this.props.onClearSearch(); - if (this.searchInput) { - this.searchInput.focus(); - } + searchInput?.current?.inputElement.current.focus(); }; - render() { - const { intl } = this.context; - const { - label, - onSearch, - onGridView, - onGridRewardsView, - onListView, - placeholder, - search, - isListView, - isGridView, - isGridRewardsView, - } = this.props; - - const gridButtonClasses = classnames([ - styles.gridView, - isGridView ? styles.selected : null, - ]); - - const gridRewardsButtonClasses = classnames([ - styles.gridRewardsView, - isGridRewardsView ? styles.selected : null, - ]); - - const listButtonClasses = classnames([ - styles.listView, - isListView ? styles.selected : null, - ]); - - const isBigSearchComponent = isListView || isGridView || isGridRewardsView; - - const searchInputClases = classnames([ - styles.searchInput, - isBigSearchComponent ? styles.inputExtrasSearch : null, - IS_GRID_REWARDS_VIEW_AVAILABLE ? styles.withGridRewardsView : null, - ]); + const hasSearchClearButton = () => { + return search.length > 0; + }; - const clearSearchClasses = classnames([ - styles.inputExtras, - isBigSearchComponent ? styles.inputExtrasSearch : null, - IS_GRID_REWARDS_VIEW_AVAILABLE ? styles.withGridRewardsView : null, - ]); + const gridButtonClasses = classnames([ + styles.gridView, + isGridView ? styles.selected : null, + ]); + const gridRewardsButtonClasses = classnames([ + styles.gridRewardsView, + isGridRewardsView ? styles.selected : null, + ]); + const isBigSearchComponent = isListView || isGridView || isGridRewardsView; + const searchInputClases = classnames([ + styles.searchInput, + isBigSearchComponent ? styles.inputExtrasSearch : null, + IS_GRID_REWARDS_VIEW_AVAILABLE ? styles.withGridRewardsView : null, + ]); + const clearSearchClasses = classnames([ + styles.inputExtras, + isBigSearchComponent ? styles.inputExtrasSearch : null, + IS_GRID_REWARDS_VIEW_AVAILABLE ? styles.withGridRewardsView : null, + ]); + return ( +
+
+ + { + searchInput.current = input; + }} + placeholder={ + placeholder || intl.formatMessage(messages.searchInputPlaceholder) + } + skin={InputSkin} + value={search} + maxLength={150} + onFocus={autoSelectOnFocus} + /> + {hasSearchClearButton && ( +
+ + + +
+ )} +
- return ( -
-
- - { - this.searchInput = input; - }} - placeholder={ - placeholder || intl.formatMessage(messages.searchInputPlaceholder) - } - skin={InputSkin} - value={search} - maxLength={150} - onFocus={this.autoSelectOnFocus} - /> - {this.hasSearchClearButton && ( -
- - - -
- )} - {isBigSearchComponent && ( -
- | - - - - {IS_GRID_REWARDS_VIEW_AVAILABLE && ( - - - - )} - - - -
+ {isBigSearchComponent && ( +
+ + + + {IS_GRID_REWARDS_VIEW_AVAILABLE && ( + + + )} +
-
- ); - } + )} +
+ ); } + +export const StakePoolsSearch = injectIntl(StakePoolsSearchComponent); diff --git a/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.messages.ts b/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.messages.ts new file mode 100644 index 0000000000..0e53b54b10 --- /dev/null +++ b/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.messages.ts @@ -0,0 +1,39 @@ +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + searchInputPlaceholder: { + id: 'staking.stakePools.search.searchInputPlaceholder', + defaultMessage: '!!!Search stake pools', + description: '"Delegating List Title" for the Stake Pools search.', + }, + delegatingListTitle: { + id: 'staking.stakePools.search.delegatingListTitle', + defaultMessage: '!!!Stake pools to which you are delegating', + description: '"delegatingListTitle" for the Stake Pools search.', + }, + listTitle: { + id: 'staking.stakePools.search.listTitle', + defaultMessage: '!!!Stake pools ({pools})', + description: '"listTitle" for the Stake Pools search.', + }, + gridIconTooltip: { + id: 'staking.stakePools.search.gridIconTooltip', + defaultMessage: '!!!Grid View', + description: '"gridIconTooltip" for the Stake Pools search.', + }, + gridRewardsIconTooltip: { + id: 'staking.stakePools.search.gridRewardsIconTooltip', + defaultMessage: '!!!Grid Rewards View', + description: '"gridRewardsIconTooltip" for the Stake Pools search.', + }, + listIconTooltip: { + id: 'staking.stakePools.search.listIconTooltip', + defaultMessage: '!!!List View', + description: '"listIconTooltip" for the Stake Pools search.', + }, + clearTooltip: { + id: 'staking.stakePools.search.clearTooltip', + defaultMessage: '!!!Clear', + description: '"clearTooltip" for the Stake Pools search.', + }, +}); diff --git a/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.scss b/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.scss index de493589ee..688a733c99 100644 --- a/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.scss +++ b/source/renderer/app/components/staking/stake-pools/StakePoolsSearch.scss @@ -1,5 +1,8 @@ +@import '../stakingConfig'; + .component { background: transparent; + display: flex; padding: 20px 0 0; position: initial; transition: position 1s ease-out, top 1s ease-out; @@ -7,6 +10,7 @@ } .container { + flex: 1 1 auto; position: relative; } @@ -49,14 +53,6 @@ line-height: 48px; position: absolute; right: 20px; - - &.inputExtrasSearch { - right: 110px; - - &.withGridRewardsView { - right: 145px; - } - } } .clearSearchButton { @@ -90,36 +86,33 @@ } .viewButtons { + @extend %contentBorderAndBackground; align-items: center; - bottom: 0.5px; display: flex; - height: 48px; + height: 50px; justify-content: space-between; line-height: 48px; - padding-left: 20px; - position: absolute; - right: 11px; + margin-left: 10px; + padding: 0 20px; + width: 118px; - .separator { - color: var(--theme-staking-stake-pools-search-clear-button-color); - opacity: 0.2; - padding-right: 10px; - position: relative; - top: -2px; + & > span { + display: flex; } button { border-radius: 3px; color: var(--theme-about-window-icon-close-button-color); cursor: pointer; - height: 28px; - margin: 0 2px; - width: 28px; + height: 15px; + width: 15px; &:hover { - background-color: var( - --theme-staking-stake-pools-search-clear-button-background-color - ); + svg { + > g > g { + opacity: 0.7; + } + } } svg { diff --git a/source/renderer/app/components/staking/stake-pools/StakePoolsSearchListViewButton.tsx b/source/renderer/app/components/staking/stake-pools/StakePoolsSearchListViewButton.tsx new file mode 100644 index 0000000000..76a6e5d265 --- /dev/null +++ b/source/renderer/app/components/staking/stake-pools/StakePoolsSearchListViewButton.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import SVGInline from 'react-svg-inline'; +import { injectIntl } from 'react-intl'; +import { PopOver } from 'react-polymorph/lib/components/PopOver'; +import classnames from 'classnames'; +// @ts-ignore ts-migrate(2307) FIXME: Cannot find module './StakePoolsSearch.scss' or it... Remove this comment to see the full error message +import styles from './StakePoolsSearch.scss'; +// @ts-ignore ts-migrate(2307) FIXME: Cannot find module '../../../assets/images/list-ic... Remove this comment to see the full error message +import listIcon from '../../../assets/images/list-ic.inline.svg'; +import type { Intl } from '../../../types/i18nTypes'; +import { messages } from './StakePoolsSearch.messages'; + +type Props = { + isListView?: boolean; + isListViewTooltipVisible?: boolean; + onClick?: () => void; + onListViewVisited?: () => void; + intl: Intl; +}; + +function StakePoolsSearchListViewButtonComponent({ + onClick, + onListViewVisited, + isListView, + isListViewTooltipVisible, + intl, +}: Props) { + const [visible, setVisible] = useState(false); + const isPopOverVisible = visible || isListViewTooltipVisible; + + const listButtonClasses = classnames([ + styles.listView, + isListView ? styles.selected : null, + ]); + + return ( + + + + ); +} + +export const StakePoolsSearchListViewButton = injectIntl( + StakePoolsSearchListViewButtonComponent +); diff --git a/source/renderer/app/containers/staking/StakePoolsListPage.js b/source/renderer/app/containers/staking/StakePoolsListPage.js index 895ab08d4d..69e1dd2ae4 100644 --- a/source/renderer/app/containers/staking/StakePoolsListPage.js +++ b/source/renderer/app/containers/staking/StakePoolsListPage.js @@ -88,6 +88,8 @@ export default class StakePoolsListPage extends Component { stake={stake} onDelegate={this.handleDelegate} isLoading={isLoading} + isListViewTooltipVisible={staking.stakePoolsListViewTooltipVisible} + onListViewVisited={staking.hideStakePoolsListViewTooltip} isFetching={isFetchingStakePools} isRanking={isRanking} getStakePoolById={getStakePoolById} diff --git a/source/renderer/app/stores/StakingStore.js b/source/renderer/app/stores/StakingStore.js index cec09fc42e..7f35ad0f66 100644 --- a/source/renderer/app/stores/StakingStore.js +++ b/source/renderer/app/stores/StakingStore.js @@ -39,14 +39,24 @@ import type { RedeemItnRewardsStep } from '../types/stakingTypes'; import type { CsvFileContent } from '../../../common/types/csv-request.types'; export default class StakingStore extends Store { - @observable isDelegationTransactionPending = false; - @observable fetchingStakePoolsFailed = false; - @observable selectedDelegationWalletId = null; - @observable stake = INITIAL_DELEGATION_FUNDS; - @observable isRanking = false; - @observable smashServerUrl: ?string = null; - @observable smashServerUrlError: ?LocalizableError = null; - @observable smashServerLoading: boolean = false; + @observable + isDelegationTransactionPending = false; + @observable + fetchingStakePoolsFailed = false; + @observable + selectedDelegationWalletId = null; + @observable + stake = INITIAL_DELEGATION_FUNDS; + @observable + isRanking = false; + @observable + smashServerUrl: string | null | undefined = null; + @observable + smashServerUrlError: LocalizableError | null | undefined = null; + @observable + smashServerLoading = false; + @observable + stakePoolsListViewTooltipVisible = true; /* ---------- Redeem ITN Rewards ---------- */ @observable redeemStep: ?RedeemItnRewardsStep = null; @@ -122,6 +132,7 @@ export default class StakingStore extends Store { this._startStakePoolsFetchTracker(); this._getStakingInfoWasOpen(); + this._getStakePoolsListViewTooltip(); } // REQUESTS @@ -248,8 +259,22 @@ export default class StakingStore extends Store { this.stakingInfoWasOpen = true; this.api.localStorage.setStakingInfoWasOpen(); }; - - @action _stakePoolsFetchTracker = () => { + @action + _getStakePoolsListViewTooltip = async () => { + const tooltipShown = await this.api.localStorage.getStakePoolsListViewTooltip(); + runInAction(() => { + this.stakePoolsListViewTooltipVisible = tooltipShown; + }); + }; + @action + hideStakePoolsListViewTooltip = () => { + this.stakePoolsListViewTooltipVisible = false; + this.api.localStorage.setStakePoolsListViewTooltip( + this.stakePoolsListViewTooltipVisible + ); + }; + @action + _stakePoolsFetchTracker = () => { const lastNumberOfStakePoolsFetched = this.numberOfStakePoolsFetched; this.numberOfStakePoolsFetched = this.stakePools.length; if (