Browse Source

Refactor ActivityIndicator

master
meriadec 7 years ago
parent
commit
1af6f5b215
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 150
      src/components/TopBar/ActivityIndicator.js
  2. 27
      src/components/TopBar/ItemContainer.js
  3. 14
      src/components/TopBar/index.js
  4. 4
      src/components/base/SideBar/SideBarListItem.js
  5. 9
      src/components/base/Spinner.js
  6. 19
      src/icons/Refresh.js
  7. 5
      static/i18n/en/common.yml

150
src/components/TopBar/ActivityIndicator.js

@ -1,61 +1,143 @@
// @flow // @flow
import React, { Component } from 'react'
import React, { Component, Fragment } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import styled from 'styled-components'
import { createStructuredSelector } from 'reselect' 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 { globalSyncStateSelector } from 'reducers/bridgeSync'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext' import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import CounterValues from 'helpers/countervalues' 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' import ItemContainer from './ItemContainer'
const Activity = styled.div` const DISPLAY_SUCCESS_TIME = 2 * 1000
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 mapStateToProps = createStructuredSelector({ globalSyncState: globalSyncStateSelector }) 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<Props, State> {
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() { render() {
const { pending, error, onClick } = this.props const { isPending, isError, t } = this.props
const { hasClicked, displaySuccess } = this.state
const isDisabled = hasClicked || displaySuccess || isError
return ( return (
<ItemContainer cursor="pointer" relative onClick={onClick}> <ItemContainer isDisabled={isDisabled} onClick={isDisabled ? undefined : this.handleRefresh}>
<IconActivity size={16} /> <Rotating
<Activity pending={pending} error={error} /> size={16}
isRotating={isPending && hasClicked}
color={isError ? 'alertRed' : displaySuccess ? 'positiveGreen' : undefined}
>
{isError ? (
<IconExclamationCircle size={16} />
) : displaySuccess ? (
<IconCheckCircle size={16} />
) : (
<IconRefresh size={16} />
)}
</Rotating>
{(displaySuccess || isError || (isPending && hasClicked)) && (
<Box
ml={2}
ff="Open Sans|SemiBold"
color={isError ? 'alertRed' : undefined}
fontSize={4}
horizontal
align="center"
>
{displaySuccess ? (
t('common:sync.upToDate')
) : isError ? (
<Fragment>
<Box>{t('common:sync.error')}</Box>
<Box
ml={2}
cursor="pointer"
style={{ textDecoration: 'underline', pointerEvents: 'all' }}
onClick={this.handleRefresh}
>
{t('common:sync.refresh')}
</Box>
</Fragment>
) : (
t('common:sync.syncing')
)}
</Box>
)}
</ItemContainer> </ItemContainer>
) )
} }
} }
const ActivityIndicator = ({ globalSyncState }: *) => ( const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => (
<BridgeSyncConsumer> <BridgeSyncConsumer>
{bridgeSync => ( {bridgeSync => (
<CounterValues.PollingConsumer> <CounterValues.PollingConsumer>
{cvPolling => ( {cvPolling => {
<ActivityIndicatorUI const isPending = cvPolling.pending || globalSyncState.pending
onClick={() => { const isError = cvPolling.error || globalSyncState.error
cvPolling.poll() return (
bridgeSync.syncAll() <ActivityIndicatorInner
}} t={t}
pending={cvPolling.pending || globalSyncState.pending} isPending={isPending}
error={cvPolling.error || globalSyncState.error} isError={!!isError && !isPending}
/> onClick={() => {
)} cvPolling.poll()
bridgeSync.syncAll()
}}
/>
)
}}
</CounterValues.PollingConsumer> </CounterValues.PollingConsumer>
)} )}
</BridgeSyncConsumer> </BridgeSyncConsumer>
) )
export default connect(mapStateToProps)(ActivityIndicator) export default compose(
translate(),
connect(mapStateToProps),
)(ActivityIndicator)

27
src/components/TopBar/ItemContainer.js

@ -2,19 +2,30 @@
import styled from 'styled-components' import styled from 'styled-components'
import Box from 'components/base/Box' import { rgba } from 'styles/helpers'
export default styled(Box).attrs({ import { Tabbable } from 'components/base/Box'
px: 2,
export default styled(Tabbable).attrs({
px: 3,
ml: 0, ml: 0,
justifyContent: 'center', alignItems: 'center',
cursor: p => (p.isInteractive ? 'pointer' : 'default'), cursor: p => (p.isDisabled ? 'default' : 'pointer'),
horizontal: true,
})` })`
opacity: 0.7; min-height: 40px;
border: 1px dashed transparent;
&:hover { &: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 { &:active {
opacity: ${p => (p.isInteractive ? 1 : 0.7)}; background: ${p => (p.isDisabled ? '' : rgba(p.theme.colors.wallet, 0.1))};
}
&:focus {
outline: none;
} }
` `

14
src/components/TopBar/index.js

@ -10,7 +10,6 @@ import { withRouter } from 'react-router'
import type { Location, RouterHistory } from 'react-router' import type { Location, RouterHistory } from 'react-router'
import type { T } from 'types/common' import type { T } from 'types/common'
import { rgba } from 'styles/helpers'
import { lock } from 'reducers/application' import { lock } from 'reducers/application'
import { hasPassword } from 'reducers/settings' import { hasPassword } from 'reducers/settings'
import { openModal } from 'reducers/modals' import { openModal } from 'reducers/modals'
@ -40,7 +39,7 @@ const Inner = styled(Box).attrs({
grow: true, grow: true,
flow: 4, 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` const Bar = styled.div`
@ -121,6 +120,11 @@ class TopBar extends PureComponent<Props> {
} }
} }
export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps), translate())( export default compose(
TopBar, withRouter,
) connect(
mapStateToProps,
mapDispatchToProps,
),
translate(),
)(TopBar)

4
src/components/base/SideBar/SideBarListItem.js

@ -75,9 +75,9 @@ const Container = styled(Tabbable).attrs({
opacity: ${p => (p.isActive ? 1 : 0.7)}; opacity: ${p => (p.isActive ? 1 : 0.7)};
} }
border: 1px dashed rgba(0, 0, 0, 0); border: 1px dashed transparent;
&:focus { &:focus {
border: 1px dashed rgba(0, 0, 0, 0.2); border-color: rgba(0, 0, 0, 0.2);
outline: none; outline: none;
} }

9
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; width: ${p => p.size}px;
height: ${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 }) { export default function Spinner({ size, ...props }: { size: number }) {
return ( return (
<Container size={size} {...props}> <Rotating size={size} {...props}>
<IconLoader size={size} /> <IconLoader size={size} />
</Container> </Rotating>
) )
} }

19
src/icons/Refresh.js

@ -0,0 +1,19 @@
// @flow
import React from 'react'
const path = (
<g transform="translate(374.01 -80.021)">
<path
fill="currentColor"
d="m-360.1 81.906a0.67329 0.67329 0 0 1 1.3465 0v3.5909a0.67329 0.67329 0 0 1-0.67325 0.67325h-3.5909a0.6733 0.6733 0 0 1 0-1.3466h2.9177zm-11.82 6.6575v2.9176a0.67332 0.67332 0 1 1-1.3466 0v-3.5909a0.67329 0.67329 0 0 1 0.67326-0.67325h3.5908a0.67329 0.67329 0 0 1 0 1.3466zm1.4633-3.441a0.67329 0.67329 0 0 1-1.2696-0.44884 6.0597 6.0597 0 0 1 9.9827-2.2775l2.7775 2.6097a0.67329 0.67329 0 1 1-0.92204 0.98128l-2.792-2.6241a4.7131 4.7131 0 0 0-7.7769 1.7596zm-2.5981 3.2587a0.67329 0.67329 0 1 1 0.92203-0.98116l2.7919 2.624a4.7131 4.7131 0 0 0 7.777-1.7595 0.67329 0.67329 0 1 1 1.2696 0.44884 6.0597 6.0597 0 0 1-9.9826 2.2775l-2.7786-2.6097z"
strokeWidth=".89773"
/>
</g>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 16 13.344" height={size} width={size} {...p}>
{path}
</svg>
)

5
static/i18n/en/common.yml

@ -28,3 +28,8 @@ lockScreen:
subTitle: Your application is locked subTitle: Your application is locked
description: Please enter your password to continue description: Please enter your password to continue
inputPlaceholder: Type your password inputPlaceholder: Type your password
sync:
syncing: Syncing...
upToDate: Up to date
error: Sync error.
refresh: Refresh

Loading…
Cancel
Save