Browse Source

Merge pull request #472 from meriadec/design/sync

Refactor ActivityIndicator
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
5311b9157f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  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
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<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() {
const { pending, error, onClick } = this.props
const { isPending, isError, t } = this.props
const { hasClicked, displaySuccess } = this.state
const isDisabled = hasClicked || displaySuccess || isError
return (
<ItemContainer cursor="pointer" relative onClick={onClick}>
<IconActivity size={16} />
<Activity pending={pending} error={error} />
<ItemContainer isDisabled={isDisabled} onClick={isDisabled ? undefined : this.handleRefresh}>
<Rotating
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>
)
}
}
const ActivityIndicator = ({ globalSyncState }: *) => (
const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => (
<BridgeSyncConsumer>
{bridgeSync => (
<CounterValues.PollingConsumer>
{cvPolling => (
<ActivityIndicatorUI
onClick={() => {
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 (
<ActivityIndicatorInner
t={t}
isPending={isPending}
isError={!!isError && !isPending}
onClick={() => {
cvPolling.poll()
bridgeSync.syncAll()
}}
/>
)
}}
</CounterValues.PollingConsumer>
)}
</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 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;
}
`

14
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<Props> {
}
}
export default compose(withRouter, connect(mapStateToProps, mapDispatchToProps), translate())(
TopBar,
)
export default compose(
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)};
}
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;
}

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;
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 (
<Container size={size} {...props}>
<Rotating size={size} {...props}>
<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
description: Please enter your password to continue
inputPlaceholder: Type your password
sync:
syncing: Syncing...
upToDate: Up to date
error: Sync error.
refresh: Refresh

Loading…
Cancel
Save