Browse Source

Merge pull request #701 from gre/proposal-up-to-date

Add 'Outdated' concept
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
fcf8d6d2ac
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 78
      src/components/TopBar/ActivityIndicator.js
  2. 2
      src/config/constants.js
  3. 3
      src/helpers/init-libcore.js
  4. 9
      src/helpers/libcore.js
  5. 16
      src/reducers/accounts.js
  6. 1
      static/i18n/en/app.yml

78
src/components/TopBar/ActivityIndicator.js

@ -10,6 +10,7 @@ import type { T } from 'types/common'
import type { AsyncState } from 'reducers/bridgeSync' import type { AsyncState } from 'reducers/bridgeSync'
import { globalSyncStateSelector } from 'reducers/bridgeSync' import { globalSyncStateSelector } from 'reducers/bridgeSync'
import { isUpToDateSelector } from 'reducers/accounts'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext' import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import CounterValues from 'helpers/countervalues' import CounterValues from 'helpers/countervalues'
@ -24,78 +25,45 @@ import ItemContainer from './ItemContainer'
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
globalSyncState: globalSyncStateSelector, globalSyncState: globalSyncStateSelector,
isUpToDate: isUpToDateSelector,
}) })
type Props = { type Props = {
// FIXME: eslint should see that it is used in static method
isGlobalSyncStatePending: boolean, // eslint-disable-line react/no-unused-prop-types
error: ?Error, error: ?Error,
isPending: boolean, isPending: boolean,
isError: boolean, isError: boolean,
isUpToDate: boolean,
t: T, t: T,
cvPoll: *, cvPoll: *,
setSyncBehavior: *, setSyncBehavior: *,
} }
type State = { class ActivityIndicatorInner extends PureComponent<Props> {
hasClicked: boolean,
isGlobalSyncStatePending: boolean,
isFirstSync: boolean,
}
class ActivityIndicatorInner extends PureComponent<Props, State> {
state = {
hasClicked: false,
isFirstSync: true,
// FIXME: eslint should see that it is used in static method
isGlobalSyncStatePending: false, // eslint-disable-line react/no-unused-state
}
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const nextState = {
...prevState,
isGlobalSyncStatePending: nextProps.isGlobalSyncStatePending,
}
if (prevState.isGlobalSyncStatePending && !nextProps.isGlobalSyncStatePending) {
nextState.isFirstSync = false
nextState.hasClicked = false
}
return nextState
}
onClick = () => { onClick = () => {
this.props.cvPoll() this.props.cvPoll()
this.props.setSyncBehavior({ type: 'SYNC_ALL_ACCOUNTS', priority: 5 }) this.props.setSyncBehavior({ type: 'SYNC_ALL_ACCOUNTS', priority: 5 })
} }
handleRefresh = () => {
this.setState({ hasClicked: true })
this.onClick()
}
render() { render() {
const { isPending, isError, error, t } = this.props const { isUpToDate, isPending, isError, error, t } = this.props
const { hasClicked, isFirstSync } = this.state const isDisabled = isError || isPending
const isDisabled = isError || (isPending && (isFirstSync || hasClicked)) const isRotating = isPending
const isRotating = isPending && (hasClicked || isFirstSync)
const content = ( const content = (
<ItemContainer disabled={isDisabled} onClick={isDisabled ? undefined : this.handleRefresh}> <ItemContainer disabled={isDisabled} onClick={isDisabled ? undefined : this.onClick}>
<Rotating <Rotating
size={16} size={16}
isRotating={isRotating} isRotating={isRotating}
color={isError ? 'alertRed' : isRotating ? 'grey' : 'positiveGreen'} color={isError ? 'alertRed' : isRotating ? 'grey' : isUpToDate ? 'positiveGreen' : 'grey'}
> >
{isError ? ( {isError ? (
<IconExclamationCircle size={16} /> <IconExclamationCircle size={16} />
) : isRotating ? ( ) : isRotating ? (
<IconLoader size={16} /> <IconLoader size={16} />
) : ( ) : isUpToDate ? (
<IconCheckCircle size={16} /> <IconCheckCircle size={16} />
) : (
<IconExclamationCircle size={16} />
)} )}
</Rotating> </Rotating>
<Box <Box
@ -115,19 +83,21 @@ class ActivityIndicatorInner extends PureComponent<Props, State> {
ml={2} ml={2}
cursor="pointer" cursor="pointer"
style={{ textDecoration: 'underline', pointerEvents: 'all' }} style={{ textDecoration: 'underline', pointerEvents: 'all' }}
onClick={this.handleRefresh} onClick={this.onClick}
> >
{t('app:common.sync.refresh')} {t('app:common.sync.refresh')}
</Box> </Box>
</Fragment> </Fragment>
) : ( ) : isUpToDate ? (
t('app:common.sync.upToDate') t('app:common.sync.upToDate')
) : (
t('app:common.sync.outdated')
)} )}
</Box> </Box>
</ItemContainer> </ItemContainer>
) )
if (error) { if (isError && error) {
return ( return (
<Tooltip <Tooltip
tooltipBg="alertRed" tooltipBg="alertRed"
@ -146,7 +116,15 @@ class ActivityIndicatorInner extends PureComponent<Props, State> {
} }
} }
const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => ( const ActivityIndicator = ({
globalSyncState,
t,
isUpToDate,
}: {
globalSyncState: AsyncState,
t: T,
isUpToDate: boolean,
}) => (
<BridgeSyncConsumer> <BridgeSyncConsumer>
{setSyncBehavior => ( {setSyncBehavior => (
<CounterValues.PollingConsumer> <CounterValues.PollingConsumer>
@ -156,9 +134,9 @@ const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState
return ( return (
<ActivityIndicatorInner <ActivityIndicatorInner
t={t} t={t}
isUpToDate={isUpToDate}
isPending={isPending} isPending={isPending}
isGlobalSyncStatePending={globalSyncState.pending} isError={!!isError && !isUpToDate} // we only show error if it's not up to date. this hide a bit error that happen from time to time
isError={!!isError}
error={isError ? globalSyncState.error : null} error={isError ? globalSyncState.error : null}
cvPoll={cvPolling.poll} cvPoll={cvPolling.poll}
setSyncBehavior={setSyncBehavior} setSyncBehavior={setSyncBehavior}

2
src/config/constants.js

@ -28,6 +28,7 @@ export const SYNC_BOOT_DELAY = 2 * 1000
export const SYNC_ALL_INTERVAL = 120 * 1000 export const SYNC_ALL_INTERVAL = 120 * 1000
export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000) export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000) export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000)
export const OUTDATED_CONSIDERED_DELAY = intFromEnv('OUTDATED_CONSIDERED_DELAY', 5 * 60 * 1000)
export const CHECK_APP_INTERVAL_WHEN_INVALID = 600 export const CHECK_APP_INTERVAL_WHEN_INVALID = 600
export const CHECK_APP_INTERVAL_WHEN_VALID = 1200 export const CHECK_APP_INTERVAL_WHEN_VALID = 1200
@ -65,6 +66,7 @@ export const DEBUG_ACTION = boolFromEnv('DEBUG_ACTION')
export const DEBUG_TAB_KEY = boolFromEnv('DEBUG_TAB_KEY') export const DEBUG_TAB_KEY = boolFromEnv('DEBUG_TAB_KEY')
export const DEBUG_LIBCORE = boolFromEnv('DEBUG_LIBCORE') export const DEBUG_LIBCORE = boolFromEnv('DEBUG_LIBCORE')
export const DEBUG_WS = boolFromEnv('DEBUG_WS') export const DEBUG_WS = boolFromEnv('DEBUG_WS')
export const DEBUG_SYNC = boolFromEnv('DEBUG_SYNC')
export const LEDGER_RESET_ALL = boolFromEnv('LEDGER_RESET_ALL') export const LEDGER_RESET_ALL = boolFromEnv('LEDGER_RESET_ALL')
export const LEDGER_DEBUG_ALL_LANGS = boolFromEnv('LEDGER_DEBUG_ALL_LANGS') export const LEDGER_DEBUG_ALL_LANGS = boolFromEnv('LEDGER_DEBUG_ALL_LANGS')
export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE') export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE')

3
src/helpers/init-libcore.js

@ -3,6 +3,7 @@
import logger from 'logger' import logger from 'logger'
import invariant from 'invariant' import invariant from 'invariant'
import network from 'api/network' import network from 'api/network'
import { serializeError } from './errors'
const lib = require('@ledgerhq/ledger-core') const lib = require('@ledgerhq/ledger-core')
@ -96,7 +97,7 @@ const NJSHttpClient = new lib.NJSHttpClient({
r.complete(urlConnection, null) r.complete(urlConnection, null)
} catch (err) { } catch (err) {
const urlConnection = createHttpConnection(res, err.message) const urlConnection = createHttpConnection(res, err.message)
r.complete(urlConnection, { code: 0, message: err.message }) r.complete(urlConnection, { code: 0, message: JSON.stringify(serializeError(err)) })
} }
}, },
}) })

9
src/helpers/libcore.js

@ -11,8 +11,7 @@ import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgerc
import { isSegwitAccount } from 'helpers/bip32' import { isSegwitAccount } from 'helpers/bip32'
import * as accountIdHelper from 'helpers/accountId' import * as accountIdHelper from 'helpers/accountId'
import { createCustomErrorClass } from './errors' import { createCustomErrorClass, deserializeError } from './errors'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName' import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName'
const NoAddressesFound = createCustomErrorClass('NoAddressesFound') const NoAddressesFound = createCustomErrorClass('NoAddressesFound')
@ -158,7 +157,11 @@ const coreSyncAccount = (core, account) =>
(payload && payload.getString('EV_SYNC_ERROR_MESSAGE')) || (payload && payload.getString('EV_SYNC_ERROR_MESSAGE')) ||
'Sync failed' 'Sync failed'
).replace(' (EC_PRIV_KEY_INVALID_FORMAT)', '') ).replace(' (EC_PRIV_KEY_INVALID_FORMAT)', '')
reject(new Error(message)) try {
reject(deserializeError(JSON.parse(message)))
} catch (e) {
reject(message)
}
return return
} }
if ( if (

16
src/reducers/accounts.js

@ -4,8 +4,8 @@ import { createSelector } from 'reselect'
import { handleActions } from 'redux-actions' import { handleActions } from 'redux-actions'
import { createAccountModel } from '@ledgerhq/live-common/lib/models/account' import { createAccountModel } from '@ledgerhq/live-common/lib/models/account'
import logger from 'logger' import logger from 'logger'
import type { Account, AccountRaw } from '@ledgerhq/live-common/lib/types' import type { Account, AccountRaw } from '@ledgerhq/live-common/lib/types'
import { OUTDATED_CONSIDERED_DELAY, DEBUG_SYNC } from 'config/constants'
export type AccountsState = Account[] export type AccountsState = Account[]
const state: AccountsState = [] const state: AccountsState = []
@ -62,6 +62,20 @@ const handlers: Object = {
export const accountsSelector = (state: { accounts: AccountsState }): Account[] => state.accounts export const accountsSelector = (state: { accounts: AccountsState }): Account[] => state.accounts
export const isUpToDateSelector = createSelector(accountsSelector, accounts =>
accounts.every(a => {
const { lastSyncDate } = a
const { blockAvgTime } = a.currency
if (!blockAvgTime) return true
const outdated =
Date.now() - (lastSyncDate || 0) > blockAvgTime * 1000 + OUTDATED_CONSIDERED_DELAY
if (outdated && DEBUG_SYNC) {
logger.log('account not up to date', a)
}
return !outdated
}),
)
export const hasAccountsSelector = createSelector(accountsSelector, accounts => accounts.length > 0) export const hasAccountsSelector = createSelector(accountsSelector, accounts => accounts.length > 0)
export const currenciesSelector = createSelector(accountsSelector, accounts => export const currenciesSelector = createSelector(accountsSelector, accounts =>

1
static/i18n/en/app.yml

@ -40,6 +40,7 @@ common:
sync: sync:
syncing: Synchronizing... syncing: Synchronizing...
upToDate: Up to date upToDate: Up to date
outdated: Outdated
error: Synchronization error error: Synchronization error
refresh: Refresh refresh: Refresh
ago: Synced {{time}} ago: Synced {{time}}

Loading…
Cancel
Save