Browse Source

Merge pull request #625 from meriadec/polishes

Polishes
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
c6abae8338
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/components/AccountPage/index.js
  2. 38
      src/components/DeviceBusyIndicator.js
  3. 37
      src/components/LibcoreBusyIndicator.js
  4. 3
      src/components/SelectAccount/index.js
  5. 3
      src/components/SelectCurrency/index.js
  6. 4
      src/components/SelectExchange.js
  7. 2
      src/components/SettingsPage/sections/Display.js
  8. 55
      src/components/TopBar/ActivityIndicator.js
  9. 17
      src/components/TopBar/index.js
  10. 8
      src/components/base/Button/index.js
  11. 1
      src/components/base/Modal/ModalTitle.js
  12. 5
      src/components/layout/Default.js
  13. 85
      src/components/modals/AddAccounts/steps/03-step-import.js
  14. 1
      src/config/constants.js
  15. 19
      src/helpers/deviceAccess.js
  16. 20
      src/helpers/withLibcore.js
  17. 21
      src/main/bridge.js
  18. 16
      src/renderer/events.js
  19. 10
      static/i18n/en/app.yml

8
src/components/AccountPage/index.js

@ -129,11 +129,9 @@ class AccountPage extends PureComponent<Props> {
)}
<Tooltip render={() => t('app:account.settings.title')}>
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}>
<Button small outlineGrey>
<Box justifyContent="center">
<IconAccountSettings size={16} />
</Box>
</Button>
<Box justifyContent="center">
<IconAccountSettings size={16} />
</Box>
</ButtonSettings>
</Tooltip>
</Box>

38
src/components/DeviceBusyIndicator.js

@ -0,0 +1,38 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
const Indicator = styled.div`
opacity: ${p => (p.busy ? 0.2 : 0)};
width: 6px;
height: 6px;
border-radius: 3px;
background-color: black;
position: fixed;
bottom: 4px;
right: 4px;
z-index: 999;
`
// NB this is done like this to be extremely performant. we don't want redux for this..
const perPaths = {}
const instances = []
export const onSetDeviceBusy = (path, busy) => {
perPaths[path] = busy
instances.forEach(i => i.forceUpdate())
}
class DeviceBusyIndicator extends PureComponent<{}> {
componentDidMount() {
instances.push(this)
}
componentWillUnmount() {
const i = instances.indexOf(this)
instances.splice(i, 1)
}
render() {
const busy = Object.values(perPaths).reduce((busy, b) => busy || b, false)
return <Indicator busy={busy} />
}
}
export default DeviceBusyIndicator

37
src/components/LibcoreBusyIndicator.js

@ -0,0 +1,37 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
const Indicator = styled.div`
opacity: ${p => (p.busy ? 0.2 : 0)};
width: 6px;
height: 6px;
border-radius: 3px;
background-color: black;
position: fixed;
bottom: 4px;
right: 4px;
z-index: 999;
`
// NB this is done like this to be extremely performant. we don't want redux for this..
let busy = false
const instances = []
export const onSetLibcoreBusy = b => {
busy = b
instances.forEach(i => i.forceUpdate())
}
class LibcoreBusyIndicator extends PureComponent<{}> {
componentDidMount() {
instances.push(this)
}
componentWillUnmount() {
const i = instances.indexOf(this)
instances.splice(i, 1)
}
render() {
return <Indicator busy={busy} />
}
}
export default LibcoreBusyIndicator

3
src/components/SelectAccount/index.js

@ -69,6 +69,9 @@ const RawSelectAccount = ({ accounts, onChange, value, t, ...props }: Props) =>
renderValue={renderOption}
renderOption={renderOption}
placeholder={t('app:common.selectAccount')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectAccountNoOption', { accountName: inputValue })
}
onChange={onChange}
/>
)

3
src/components/SelectCurrency/index.js

@ -40,6 +40,9 @@ const SelectCurrency = ({ onChange, value, t, placeholder, currencies, ...props
renderValue={renderOption}
options={options}
placeholder={placeholder || t('app:common.selectCurrency')}
noOptionsMessage={({ inputValue }: { inputValue: string }) =>
t('app:common.selectCurrencyNoOption', { currencyName: inputValue })
}
onChange={item => onChange(item ? item.currency : null)}
{...props}
/>

4
src/components/SelectExchange.js

@ -93,6 +93,10 @@ class SelectExchange extends Component<
options={options}
onChange={onChange}
isLoading={options.length === 0}
placeholder={t('app:common.selectExchange')}
noOptionsMessage={({ inputValue }) =>
t('app:common.selectExchangeNoOption', { exchangeName: inputValue })
}
{...props}
/>
)

2
src/components/SettingsPage/sections/Display.js

@ -169,7 +169,7 @@ class TabProfile extends PureComponent<Props, State> {
to={counterValueCurrency}
exchangeId={counterValueExchange}
onChange={this.handleChangeExchange}
minWidth={150}
minWidth={200}
/>
</Box>
</Row>

55
src/components/TopBar/ActivityIndicator.js

@ -10,7 +10,6 @@ import type { T } from 'types/common'
import type { AsyncState } from 'reducers/bridgeSync'
import { globalSyncStateSelector } from 'reducers/bridgeSync'
import { hasAccountsSelector } from 'reducers/accounts'
import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext'
import CounterValues from 'helpers/countervalues'
@ -23,7 +22,6 @@ import ItemContainer from './ItemContainer'
const mapStateToProps = createStructuredSelector({
globalSyncState: globalSyncStateSelector,
hasAccounts: hasAccountsSelector,
})
type Props = {
@ -128,37 +126,28 @@ class ActivityIndicatorInner extends PureComponent<Props, State> {
}
}
const ActivityIndicator = ({
globalSyncState,
hasAccounts,
t,
}: {
globalSyncState: AsyncState,
hasAccounts: boolean,
t: T,
}) =>
!hasAccounts ? null : (
<BridgeSyncConsumer>
{setSyncBehavior => (
<CounterValues.PollingConsumer>
{cvPolling => {
const isPending = cvPolling.pending || globalSyncState.pending
const isError = cvPolling.error || globalSyncState.error
return (
<ActivityIndicatorInner
t={t}
isPending={isPending}
isGlobalSyncStatePending={globalSyncState.pending}
isError={!!isError && !isPending}
cvPoll={cvPolling.poll}
setSyncBehavior={setSyncBehavior}
/>
)
}}
</CounterValues.PollingConsumer>
)}
</BridgeSyncConsumer>
)
const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => (
<BridgeSyncConsumer>
{setSyncBehavior => (
<CounterValues.PollingConsumer>
{cvPolling => {
const isPending = cvPolling.pending || globalSyncState.pending
const isError = cvPolling.error || globalSyncState.error
return (
<ActivityIndicatorInner
t={t}
isPending={isPending}
isGlobalSyncStatePending={globalSyncState.pending}
isError={!!isError && !isPending}
cvPoll={cvPolling.poll}
setSyncBehavior={setSyncBehavior}
/>
)
}}
</CounterValues.PollingConsumer>
)}
</BridgeSyncConsumer>
)
export default compose(
translate(),

17
src/components/TopBar/index.js

@ -12,6 +12,7 @@ import type { T } from 'types/common'
import { lock } from 'reducers/application'
import { hasPassword } from 'reducers/settings'
import { hasAccountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import IconLock from 'icons/Lock'
@ -54,6 +55,7 @@ const Bar = styled.div`
const mapStateToProps = state => ({
hasPassword: hasPassword(state),
hasAccounts: hasAccountsSelector(state),
})
const mapDispatchToProps = {
@ -63,6 +65,7 @@ const mapDispatchToProps = {
type Props = {
hasPassword: boolean,
hasAccounts: boolean,
history: RouterHistory,
location: Location,
lock: Function,
@ -91,17 +94,21 @@ class TopBar extends PureComponent<Props> {
}
}
render() {
const { hasPassword, t } = this.props
const { hasPassword, hasAccounts, t } = this.props
return (
<Container bg="lightGrey" color="graphite">
<Inner>
<Box grow horizontal>
<GlobalSearch t={t} isHidden />
<ActivityIndicator />
<Box justifyContent="center">
<Bar />
</Box>
{hasAccounts && (
<Fragment>
<ActivityIndicator />
<Box justifyContent="center">
<Bar />
</Box>
</Fragment>
)}
<Tooltip render={() => t('app:settings.title')}>
<ItemContainer isInteractive onClick={this.navigateToSettings}>
<IconSettings size={16} />

8
src/components/base/Button/index.js

@ -16,8 +16,12 @@ type Style = any // FIXME
const buttonStyles: { [_: string]: Style } = {
default: {
default: noop,
active: noop,
hover: noop,
active: p => `
background: ${rgba(p.theme.colors.fog, 0.3)};
`,
hover: p => `
background: ${rgba(p.theme.colors.fog, 0.2)};
`,
focus: () => `
box-shadow: ${focusedShadowStyle};
`,

1
src/components/base/Modal/ModalTitle.js

@ -30,6 +30,7 @@ const Back = styled(Box).attrs({
})`
cursor: pointer;
position: absolute;
line-height: 1;
top: 0;
left: 0;

5
src/components/layout/Default.js

@ -17,6 +17,8 @@ import DashboardPage from 'components/DashboardPage'
import ManagerPage from 'components/ManagerPage'
import ExchangePage from 'components/ExchangePage'
import SettingsPage from 'components/SettingsPage'
import LibcoreBusyIndicator from 'components/LibcoreBusyIndicator'
import DeviceBusyIndicator from 'components/DeviceBusyIndicator'
import AppRegionDrag from 'components/AppRegionDrag'
import IsUnlocked from 'components/IsUnlocked'
@ -96,6 +98,9 @@ class Default extends Component<Props> {
</Main>
</Box>
</Box>
<LibcoreBusyIndicator />
<DeviceBusyIndicator />
</IsUnlocked>
</Fragment>
)

85
src/components/modals/AddAccounts/steps/03-step-import.js

@ -11,7 +11,7 @@ import Box from 'components/base/Box'
import CurrencyBadge from 'components/base/CurrencyBadge'
import Button from 'components/base/Button'
import AccountsList from 'components/base/AccountsList'
import IconExchange from 'icons/Exchange'
import IconExclamationCircleThin from 'icons/ExclamationCircleThin'
import type { StepProps } from '../index'
@ -20,14 +20,30 @@ class StepImport extends PureComponent<StepProps> {
this.startScanAccountsDevice()
}
componentDidUpdate(prevProps: StepProps) {
// handle case when we click on stop sync
if (prevProps.scanStatus !== 'finished' && this.props.scanStatus === 'finished') {
this.unsub()
}
// handle case when we click on retry sync
if (prevProps.scanStatus !== 'scanning' && this.props.scanStatus === 'scanning') {
this.startScanAccountsDevice()
}
}
componentWillUnmount() {
this.unsub()
}
scanSubscription = null
unsub = () => {
if (this.scanSubscription) {
this.scanSubscription.unsubscribe()
}
}
scanSubscription = null
translateName(account: Account) {
const { t } = this.props
let { name } = account
@ -45,6 +61,7 @@ class StepImport extends PureComponent<StepProps> {
}
startScanAccountsDevice() {
this.unsub()
const { currency, currentDevice, setState } = this.props
try {
invariant(currency, 'No currency to scan')
@ -82,10 +99,7 @@ class StepImport extends PureComponent<StepProps> {
}
handleRetry = () => {
if (this.scanSubscription) {
this.scanSubscription.unsubscribe()
this.scanSubscription = null
}
this.unsub()
this.handleResetState()
this.startScanAccountsDevice()
}
@ -131,6 +145,17 @@ class StepImport extends PureComponent<StepProps> {
handleUnselectAll = () => this.props.setState({ checkedAccountsIds: [] })
renderError() {
const { err, t } = this.props
invariant(err, 'Trying to render inexisting error')
return (
<Box style={{ height: 200 }} align="center" justify="center" color="alertRed">
<IconExclamationCircleThin size={43} />
<Box mt={4}>{t('app:addAccounts.somethingWentWrong')}</Box>
</Box>
)
}
render() {
const {
scanStatus,
@ -142,6 +167,12 @@ class StepImport extends PureComponent<StepProps> {
t,
} = this.props
if (err) {
return this.renderError()
}
const currencyName = currency ? currency.name : ''
const importableAccounts = scannedAccounts.filter(acc => {
if (acc.operations.length <= 0) {
return false
@ -160,9 +191,8 @@ class StepImport extends PureComponent<StepProps> {
count: importableAccounts.length,
})
const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', {
currencyName: currency ? ` ${currency.name}}` : '',
})
const importableAccountsEmpty = t('app:addAccounts.noAccountToImport', { currencyName })
const hasAlreadyEmptyAccount = scannedAccounts.some(a => a.operations.length === 0)
return (
<Fragment>
@ -180,7 +210,11 @@ class StepImport extends PureComponent<StepProps> {
/>
<AccountsList
title={t('app:addAccounts.createNewAccount.title')}
emptyText={t('app:addAccounts.createNewAccount.noOperationOnLastAccount')}
emptyText={
hasAlreadyEmptyAccount
? t('app:addAccounts.createNewAccount.noOperationOnLastAccount')
: t('app:addAccounts.createNewAccount.noAccountToCreate', { currencyName })
}
accounts={creatableAccounts}
checkedIds={checkedAccountsIds}
onToggleAccount={this.handleToggleAccount}
@ -189,17 +223,7 @@ class StepImport extends PureComponent<StepProps> {
/>
</Box>
{err && (
<Box shrink>
{err.message}
<Button small outline onClick={this.handleRetry}>
<Box horizontal flow={2} align="center">
<IconExchange size={13} />
<span>{t('app:addAccounts.retrySync')}</span>
</Box>
</Button>
</Box>
)}
{err && <Box shrink>{err.message}</Box>}
</Fragment>
)
}
@ -208,6 +232,7 @@ class StepImport extends PureComponent<StepProps> {
export default StepImport
export const StepImportFooter = ({
setState,
scanStatus,
onClickAdd,
onCloseModal,
@ -250,7 +275,21 @@ export const StepImportFooter = ({
return (
<Fragment>
{currency && <CurrencyBadge mr="auto" currency={currency} />}
<Button primary disabled={scanStatus !== 'finished'} onClick={onClick}>
{scanStatus === 'error' && (
<Button mr={2} onClick={() => setState({ scanStatus: 'scanning', err: null })}>
{t('app:common.retry')}
</Button>
)}
{scanStatus === 'scanning' && (
<Button mr={2} onClick={() => setState({ scanStatus: 'finished' })}>
{t('app:common.stop')}
</Button>
)}
<Button
primary
disabled={scanStatus !== 'finished' && scanStatus !== 'error'}
onClick={onClick}
>
{ctaWording}
</Button>
</Fragment>

1
src/config/constants.js

@ -65,6 +65,7 @@ export const SKIP_GENUINE = boolFromEnv('SKIP_GENUINE')
export const SKIP_ONBOARDING = boolFromEnv('SKIP_ONBOARDING')
export const SHOW_LEGACY_NEW_ACCOUNT = boolFromEnv('SHOW_LEGACY_NEW_ACCOUNT')
export const HIGHLIGHT_I18N = boolFromEnv('HIGHLIGHT_I18N')
export const DISABLE_ACTIVITY_INDICATORS = boolFromEnv('DISABLE_ACTIVITY_INDICATORS')
// Other constants

19
src/helpers/deviceAccess.js

@ -18,7 +18,7 @@ export const withDevice: WithDevice = devicePath => {
semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1))
return job =>
takeSemaphorePromise(sem, async () => {
takeSemaphorePromise(sem, devicePath, async () => {
const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 })
if (DEBUG_DEVICE) t.setDebugMode(true)
@ -32,17 +32,32 @@ export const withDevice: WithDevice = devicePath => {
})
}
function takeSemaphorePromise<T>(sem, f: () => Promise<T>): Promise<T> {
function takeSemaphorePromise<T>(sem, devicePath, f: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
sem.take(() => {
process.send({
type: 'setDeviceBusy',
busy: true,
devicePath,
})
f().then(
r => {
sem.leave()
resolve(r)
process.send({
type: 'setDeviceBusy',
busy: false,
devicePath,
})
},
e => {
sem.leave()
reject(e)
process.send({
type: 'setDeviceBusy',
busy: false,
devicePath,
})
},
)
})

20
src/helpers/withLibcore.js

@ -3,8 +3,24 @@
// TODO: `core` should be typed
type Job<A> = Object => Promise<A>
export default function withLibcore<A>(job: Job<A>): Promise<A> {
let counter = 0
export default async function withLibcore<A>(job: Job<A>): Promise<A> {
const core = require('./init-libcore').default
core.getPoolInstance()
return job(core)
try {
if (counter++ === 0) {
process.send({
type: 'setLibcoreBusy',
busy: true,
})
}
return job(core)
} finally {
if (--counter === 0) {
process.send({
type: 'setLibcoreBusy',
busy: false,
})
}
}
}

21
src/main/bridge.js

@ -97,16 +97,19 @@ ipcMainListenReceiveCommands({
})
function handleGlobalInternalMessage(payload) {
if (payload.type === 'executeHttpQueryOnRenderer') {
const win = getMainWindow && getMainWindow()
if (!win) {
logger.warn("can't executeHttpQueryOnRenderer because no renderer")
return
switch (payload.type) {
case 'setLibcoreBusy':
case 'setDeviceBusy':
case 'executeHttpQueryOnRenderer': {
const win = getMainWindow && getMainWindow()
if (!win) {
logger.warn(`can't ${payload.type} because no renderer`)
return
}
win.webContents.send(payload.type, payload)
break
}
win.webContents.send('executeHttpQuery', {
id: payload.id,
networkArg: payload.networkArg,
})
default:
}
}

16
src/renderer/events.js

@ -15,7 +15,9 @@ import network from 'api/network'
import { ipcRenderer } from 'electron'
import debug from 'debug'
import { CHECK_UPDATE_DELAY } from 'config/constants'
import { CHECK_UPDATE_DELAY, DISABLE_ACTIVITY_INDICATORS } from 'config/constants'
import { onSetDeviceBusy } from 'components/DeviceBusyIndicator'
import { onSetLibcoreBusy } from 'components/LibcoreBusyIndicator'
import { hasPassword } from 'reducers/settings'
import { lock } from 'reducers/application'
@ -87,7 +89,7 @@ export default ({ store }: { store: Object }) => {
}
})
ipcRenderer.on('executeHttpQuery', (event: any, { networkArg, id }) => {
ipcRenderer.on('executeHttpQueryOnRenderer', (event: any, { networkArg, id }) => {
network(networkArg).then(
result => {
ipcRenderer.send('executeHttpQueryPayload', { type: 'success', id, result })
@ -98,6 +100,16 @@ export default ({ store }: { store: Object }) => {
)
})
if (!DISABLE_ACTIVITY_INDICATORS) {
ipcRenderer.on('setLibcoreBusy', (event: any, { busy }) => {
onSetLibcoreBusy(busy)
})
ipcRenderer.on('setDeviceBusy', (event: any, { busy, devicePath }) => {
onSetDeviceBusy(devicePath, busy)
})
}
if (__PROD__) {
// TODO move this to "command" pattern
const updaterHandlers = {

10
static/i18n/en/app.yml

@ -11,7 +11,11 @@ common:
chooseWalletPlaceholder: Choose a wallet...
currency: Currency
selectAccount: Select an account
selectAccountNoOption: 'No account matching "{{accountName}}"'
selectCurrency: Select a currency
selectCurrencyNoOption: 'No currency matching "{{currencyName}}"'
selectExchange: Select an exchange
selectExchangeNoOption: 'No exchange matching "{{exchangeName}}"'
sortBy: Sort by
search: Search
save: Save
@ -23,6 +27,7 @@ common:
next: Next
back: Back
retry: Retry
stop: Stop
close: Close
eastern: Eastern
western: Western
@ -138,12 +143,13 @@ addAccounts:
editName: Edit name
newAccount: New account
legacyAccount: '{{accountName}} (legacy)'
noAccountToImport: We didnt find any {{currencyName}}} account to import.
noAccountToImport: We didnt find any {{currencyName}} account to import.
success: Great success!
createNewAccount:
title: Create new account
noOperationOnLastAccount: You cannot create a new account because your last account has no operations
retrySync: Retry sync
noAccountToCreate: We didnt find any {{currencyName}} account to create.
somethingWentWrong: Something went wrong during synchronization.
cta:
create: 'Create account'
import: 'Import account'

Loading…
Cancel
Save