From 1af6f5b2154df73ed9755d4478863249d9b6fd77 Mon Sep 17 00:00:00 2001 From: meriadec Date: Thu, 7 Jun 2018 15:09:34 +0200 Subject: [PATCH] Refactor ActivityIndicator --- src/components/TopBar/ActivityIndicator.js | 150 ++++++++++++++---- src/components/TopBar/ItemContainer.js | 27 +++- src/components/TopBar/index.js | 14 +- .../base/SideBar/SideBarListItem.js | 4 +- src/components/base/Spinner.js | 9 +- src/icons/Refresh.js | 19 +++ static/i18n/en/common.yml | 5 + 7 files changed, 175 insertions(+), 53 deletions(-) create mode 100644 src/icons/Refresh.js diff --git a/src/components/TopBar/ActivityIndicator.js b/src/components/TopBar/ActivityIndicator.js index 45328010..ef061b71 100644 --- a/src/components/TopBar/ActivityIndicator.js +++ b/src/components/TopBar/ActivityIndicator.js @@ -1,61 +1,143 @@ // @flow -import React, { Component } from 'react' + +import React, { Component, Fragment } from 'react' +import { compose } from 'redux' import { connect } from 'react-redux' -import styled from 'styled-components' import { createStructuredSelector } from 'reselect' +import { translate } from 'react-i18next' + +import type { T } from 'types/common' +import type { AsyncState } from 'reducers/bridgeSync' import { globalSyncStateSelector } from 'reducers/bridgeSync' import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext' import CounterValues from 'helpers/countervalues' -import IconActivity from 'icons/Activity' + +import { Rotating } from 'components/base/Spinner' +import Box from 'components/base/Box' +import IconRefresh from 'icons/Refresh' +import IconExclamationCircle from 'icons/ExclamationCircle' +import IconCheckCircle from 'icons/CheckCircle' import ItemContainer from './ItemContainer' -const Activity = styled.div` - background: ${p => - p.pending - ? p.theme.colors.wallet - : p.error - ? p.theme.colors.alertRed - : p.theme.colors.positiveGreen}; - border-radius: 50%; - bottom: 23px; - position: absolute; - left: -5px; - width: 12px; - height: 12px; -` +const DISPLAY_SUCCESS_TIME = 2 * 1000 const mapStateToProps = createStructuredSelector({ globalSyncState: globalSyncStateSelector }) -class ActivityIndicatorUI extends Component<*> { +type Props = { isPending: boolean, isError: boolean, onClick: void => void, t: T } +type State = { hasClicked: boolean, displaySuccess: boolean } + +class ActivityIndicatorInner extends Component { + state = { + hasClicked: false, + displaySuccess: false, + } + + _timeout = null + + static getDerivedStateFromProps(nextProps: Props, prevState: State) { + if (prevState.hasClicked && !nextProps.isPending) { + return { hasClicked: false, displaySuccess: !nextProps.isError } + } + + return null + } + + componentDidUpdate(prevProps: Props, prevState: State) { + if (!prevState.displaySuccess && this.state.displaySuccess) { + if (this._timeout) { + clearTimeout(this._timeout) + } + this._timeout = setTimeout( + () => this.setState({ displaySuccess: false }), + DISPLAY_SUCCESS_TIME, + ) + } + } + + handleRefresh = () => { + const { onClick } = this.props + this.setState({ hasClicked: true }) + onClick() + } + render() { - const { pending, error, onClick } = this.props + const { isPending, isError, t } = this.props + const { hasClicked, displaySuccess } = this.state + const isDisabled = hasClicked || displaySuccess || isError return ( - - - + + + {isError ? ( + + ) : displaySuccess ? ( + + ) : ( + + )} + + {(displaySuccess || isError || (isPending && hasClicked)) && ( + + {displaySuccess ? ( + t('common:sync.upToDate') + ) : isError ? ( + + {t('common:sync.error')} + + {t('common:sync.refresh')} + + + ) : ( + t('common:sync.syncing') + )} + + )} ) } } -const ActivityIndicator = ({ globalSyncState }: *) => ( +const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => ( {bridgeSync => ( - {cvPolling => ( - { - cvPolling.poll() - bridgeSync.syncAll() - }} - pending={cvPolling.pending || globalSyncState.pending} - error={cvPolling.error || globalSyncState.error} - /> - )} + {cvPolling => { + const isPending = cvPolling.pending || globalSyncState.pending + const isError = cvPolling.error || globalSyncState.error + return ( + { + cvPolling.poll() + bridgeSync.syncAll() + }} + /> + ) + }} )} ) -export default connect(mapStateToProps)(ActivityIndicator) +export default compose( + translate(), + connect(mapStateToProps), +)(ActivityIndicator) diff --git a/src/components/TopBar/ItemContainer.js b/src/components/TopBar/ItemContainer.js index d6b750e4..70c958df 100644 --- a/src/components/TopBar/ItemContainer.js +++ b/src/components/TopBar/ItemContainer.js @@ -2,19 +2,30 @@ import styled from 'styled-components' -import Box from 'components/base/Box' +import { rgba } from 'styles/helpers' -export default styled(Box).attrs({ - px: 2, +import { Tabbable } from 'components/base/Box' + +export default styled(Tabbable).attrs({ + px: 3, ml: 0, - justifyContent: 'center', - cursor: p => (p.isInteractive ? 'pointer' : 'default'), + alignItems: 'center', + cursor: p => (p.isDisabled ? 'default' : 'pointer'), + horizontal: true, })` - opacity: 0.7; + min-height: 40px; + border: 1px dashed transparent; + &:hover { - opacity: ${p => (p.isInteractive ? 0.85 : 0.7)}; + color: ${p => (p.isDisabled ? '' : p.theme.colors.wallet)}; + background: ${p => (p.isDisabled ? '' : rgba(p.theme.colors.wallet, 0.05))}; } + &:active { - opacity: ${p => (p.isInteractive ? 1 : 0.7)}; + background: ${p => (p.isDisabled ? '' : rgba(p.theme.colors.wallet, 0.1))}; + } + + &:focus { + outline: none; } ` diff --git a/src/components/TopBar/index.js b/src/components/TopBar/index.js index a3871d2f..9db8c194 100644 --- a/src/components/TopBar/index.js +++ b/src/components/TopBar/index.js @@ -10,7 +10,6 @@ import { withRouter } from 'react-router' import type { Location, RouterHistory } from 'react-router' import type { T } from 'types/common' -import { rgba } from 'styles/helpers' import { lock } from 'reducers/application' import { hasPassword } from 'reducers/settings' import { openModal } from 'reducers/modals' @@ -40,7 +39,7 @@ const Inner = styled(Box).attrs({ grow: true, flow: 4, })` - border-bottom: 1px solid ${p => rgba(p.theme.colors.black, 0.15)}; + border-bottom: 1px solid ${p => p.theme.colors.fog}; ` const Bar = styled.div` @@ -121,6 +120,11 @@ class TopBar extends PureComponent { } } -export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps), translate())( - TopBar, -) +export default compose( + withRouter, + connect( + mapStateToProps, + mapDispatchToProps, + ), + translate(), +)(TopBar) diff --git a/src/components/base/SideBar/SideBarListItem.js b/src/components/base/SideBar/SideBarListItem.js index 85e6e91e..0121940b 100644 --- a/src/components/base/SideBar/SideBarListItem.js +++ b/src/components/base/SideBar/SideBarListItem.js @@ -75,9 +75,9 @@ const Container = styled(Tabbable).attrs({ opacity: ${p => (p.isActive ? 1 : 0.7)}; } - border: 1px dashed rgba(0, 0, 0, 0); + border: 1px dashed transparent; &:focus { - border: 1px dashed rgba(0, 0, 0, 0.2); + border-color: rgba(0, 0, 0, 0.2); outline: none; } diff --git a/src/components/base/Spinner.js b/src/components/base/Spinner.js index 6d3cec98..d878f542 100644 --- a/src/components/base/Spinner.js +++ b/src/components/base/Spinner.js @@ -15,16 +15,17 @@ const rotate = keyframes` } ` -const Container = styled(Box)` +export const Rotating = styled(Box)` width: ${p => p.size}px; height: ${p => p.size}px; - animation: ${rotate} 1.5s linear infinite; + animation: ${p => (p.isRotating === false ? 'none' : `${rotate} 1.5s linear infinite`)}; + transition: 100ms linear transform; ` export default function Spinner({ size, ...props }: { size: number }) { return ( - + - + ) } diff --git a/src/icons/Refresh.js b/src/icons/Refresh.js new file mode 100644 index 00000000..d86ae547 --- /dev/null +++ b/src/icons/Refresh.js @@ -0,0 +1,19 @@ +// @flow + +import React from 'react' + +const path = ( + + + +) + +export default ({ size, ...p }: { size: number }) => ( + + {path} + +) diff --git a/static/i18n/en/common.yml b/static/i18n/en/common.yml index a84460d5..c5add684 100644 --- a/static/i18n/en/common.yml +++ b/static/i18n/en/common.yml @@ -28,3 +28,8 @@ lockScreen: subTitle: Your application is locked description: Please enter your password to continue inputPlaceholder: Type your password +sync: + syncing: Syncing... + upToDate: Up to date + error: Sync error. + refresh: Refresh