Browse Source

Optimize performance of OperationsList

master
Gaëtan Renaudeau 7 years ago
parent
commit
39a051e62f
  1. 4
      src/components/CalculateBalance.js
  2. 40
      src/components/OperationsList/AccountCell.js
  3. 62
      src/components/OperationsList/AddressCell.js
  4. 51
      src/components/OperationsList/AmountCell.js
  5. 68
      src/components/OperationsList/ConfirmationCell.js
  6. 70
      src/components/OperationsList/ConfirmationCheck.js
  7. 40
      src/components/OperationsList/DateCell.js
  8. 186
      src/components/OperationsList/Operation.js
  9. 31
      src/components/OperationsList/SectionTitle.js
  10. 77
      src/components/OperationsList/index.js
  11. 62
      src/components/modals/OperationDetails.js

4
src/components/CalculateBalance.js

@ -1,7 +1,7 @@
// @flow
/* eslint-disable react/no-unused-prop-types */
import { PureComponent } from 'react'
import { Component } from 'react'
import { connect } from 'react-redux'
import type { Account } from '@ledgerhq/live-common/lib/types'
@ -79,7 +79,7 @@ const mapStateToProps = (state: State, props: OwnProps) => {
}
}
class CalculateBalance extends PureComponent<Props> {
class CalculateBalance extends Component<Props> {
render() {
const { children } = this.props
return children(this.props)

40
src/components/OperationsList/AccountCell.js

@ -0,0 +1,40 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
const Cell = styled(Box).attrs({
px: 4,
horizontal: true,
alignItems: 'center',
})`
width: 150px;
overflow: hidden;
`
type Props = {
currency: Currency,
accountName: string,
}
class AccountCell extends PureComponent<Props> {
render() {
const { currency, accountName } = this.props
const Icon = getCryptoCurrencyIcon(currency)
return (
<Cell horizontal flow={2}>
<Box alignItems="center" justifyContent="center" style={{ color: currency.color }}>
{Icon && <Icon size={16} />}
</Box>
<Box ff="Open Sans|SemiBold" fontSize={3} color="dark">
{accountName}
</Box>
</Cell>
)
}
}
export default AccountCell

62
src/components/OperationsList/AddressCell.js

@ -0,0 +1,62 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Operation } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
const Address = ({ value }: { value: string }) => {
if (!value) {
return <Box />
}
const addrSize = value.length / 2
// FIXME why not using CSS for this? meaning we might be able to have a left & right which both take 50% & play with overflow & text-align
const left = value.slice(0, 10)
const right = value.slice(-addrSize)
const middle = value.slice(10, -addrSize)
return (
<Box horizontal color="smoke" ff="Open Sans" fontSize={3}>
<div>{left}</div>
<AddressEllipsis>{middle}</AddressEllipsis>
<div>{right}</div>
</Box>
)
}
const AddressEllipsis = styled.div`
display: block;
flex-shrink: 1;
min-width: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const Cell = styled(Box).attrs({
px: 4,
horizontal: true,
alignItems: 'center',
})`
width: 150px;
`
type Props = {
operation: Operation,
}
class AddressCell extends PureComponent<Props> {
render() {
const { operation } = this.props
return (
<Cell grow shrink style={{ display: 'block' }}>
<Address value={operation.type === 'IN' ? operation.senders[0] : operation.recipients[0]} />
</Cell>
)
}
}
export default AddressCell

51
src/components/OperationsList/AmountCell.js

@ -0,0 +1,51 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation'
import type { Currency, Unit, Operation } from '@ledgerhq/live-common/lib/types'
import Box from 'components/base/Box'
import CounterValue from 'components/CounterValue'
import FormattedVal from 'components/base/FormattedVal'
const Cell = styled(Box).attrs({
px: 4,
horizontal: false,
alignItems: 'flex-end',
})`
width: 150px;
`
type Props = {
operation: Operation,
currency: Currency,
unit: Unit,
}
class AmountCell extends PureComponent<Props> {
render() {
const { currency, unit, operation } = this.props
const amount = getOperationAmountNumber(operation)
return (
<Cell>
<FormattedVal
val={amount}
unit={unit}
showCode
fontSize={4}
alwaysShowSign
color={amount < 0 ? 'smoke' : undefined}
/>
<CounterValue
color="grey"
fontSize={3}
date={operation.date}
currency={currency}
value={amount}
/>
</Cell>
)
}
}
export default AmountCell

68
src/components/OperationsList/ConfirmationCell.js

@ -0,0 +1,68 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { createStructuredSelector } from 'reselect'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import type { T, CurrencySettings } from 'types/common'
import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'reducers/settings'
import { getMarketColor } from 'styles/helpers'
import Box from 'components/base/Box'
import ConfirmationCheck from './ConfirmationCheck'
const mapStateToProps = createStructuredSelector({
currencySettings: currencySettingsForAccountSelector,
marketIndicator: marketIndicatorSelector,
})
const Cell = styled(Box).attrs({
px: 4,
horizontal: true,
alignItems: 'center',
})`
width: 44px;
`
type Props = {
account: Account,
currencySettings: CurrencySettings,
marketIndicator: string,
t: T,
operation: Operation,
}
class ConfirmationCell extends PureComponent<Props> {
render() {
const { account, currencySettings, t, operation, marketIndicator } = this.props
const isNegative = operation.type === 'OUT'
const isConfirmed =
(operation.blockHeight ? account.blockHeight - operation.blockHeight : 0) >
currencySettings.confirmationsNb
const marketColor = getMarketColor({
marketIndicator,
isNegative,
})
return (
<Cell align="center" justify="flex-start">
<ConfirmationCheck
type={operation.type}
isConfirmed={isConfirmed}
marketColor={marketColor}
t={t}
/>
</Cell>
)
}
}
export default connect(mapStateToProps)(ConfirmationCell)

70
src/components/OperationsList/ConfirmationCheck.js

@ -1,6 +1,6 @@
// @flow
import React from 'react'
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { OperationType } from '@ledgerhq/live-common/lib/types'
@ -16,6 +16,11 @@ import IconSend from 'icons/Send'
import Box from 'components/base/Box'
import Tooltip from 'components/base/Tooltip'
const border = p =>
p.isConfirmed
? 0
: `1px solid ${p.type === 'IN' ? p.marketColor : rgba(p.theme.colors.grey, 0.2)}`
const Container = styled(Box).attrs({
bg: p =>
p.isConfirmed ? rgba(p.type === 'IN' ? p.marketColor : p.theme.colors.grey, 0.2) : 'none',
@ -23,10 +28,7 @@ const Container = styled(Box).attrs({
align: 'center',
justify: 'center',
})`
border: ${p =>
!p.isConfirmed
? `1px solid ${p.type === 'IN' ? p.marketColor : rgba(p.theme.colors.grey, 0.2)}`
: 0};
border: ${border};
border-radius: 50%;
position: relative;
height: 24px;
@ -44,46 +46,38 @@ const WrapperClock = styled(Box).attrs({
padding: 1px;
`
const ConfirmationCheck = ({
marketColor,
isConfirmed,
t,
type,
withTooltip,
...props
}: {
class ConfirmationCheck extends PureComponent<{
marketColor: string,
isConfirmed: boolean,
t: T,
type: OperationType,
withTooltip?: boolean,
}) => {
const renderContent = () => (
<Container type={type} isConfirmed={isConfirmed} marketColor={marketColor} {...props}>
{type === 'IN' ? <IconReceive size={12} /> : <IconSend size={12} />}
{!isConfirmed && (
<WrapperClock>
<IconClock size={10} />
</WrapperClock>
)}
</Container>
)
}> {
static defaultProps = {
withTooltip: true,
}
return withTooltip ? (
<Tooltip
render={() =>
isConfirmed ? t('operationsList:confirmed') : t('operationsList:notConfirmed')
}
>
{renderContent()}
</Tooltip>
) : (
renderContent()
)
}
renderTooltip = () => {
const { t, isConfirmed } = this.props
return t(isConfirmed ? 'operationsList:confirmed' : 'operationsList:notConfirmed')
}
render() {
const { marketColor, isConfirmed, t, type, withTooltip, ...props } = this.props
const content = (
<Container type={type} isConfirmed={isConfirmed} marketColor={marketColor} {...props}>
{type === 'IN' ? <IconReceive size={12} /> : <IconSend size={12} />}
{!isConfirmed && (
<WrapperClock>
<IconClock size={10} />
</WrapperClock>
)}
</Container>
)
ConfirmationCheck.defaultProps = {
withTooltip: true,
return withTooltip ? <Tooltip render={this.renderTooltip}>{content}</Tooltip> : content
}
}
export default ConfirmationCheck

40
src/components/OperationsList/DateCell.js

@ -0,0 +1,40 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import type { Operation } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import OperationDate from './OperationDate'
const Cell = styled(Box).attrs({
px: 3,
horizontal: false,
})`
width: 120px;
`
type Props = {
t: T,
operation: Operation,
}
class DateCell extends PureComponent<Props> {
static defaultProps = {
withAccount: false,
}
render() {
const { t, operation } = this.props
return (
<Cell>
<Box ff="Open Sans|SemiBold" fontSize={3} color="smoke">
{t(`operationsList:${operation.type}`)}
</Box>
<OperationDate date={operation.date} />
</Cell>
)
}
}
export default DateCell

186
src/components/OperationsList/Operation.js

@ -1,36 +1,17 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { createStructuredSelector } from 'reselect'
import noop from 'lodash/noop'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import type { T, CurrencySettings } from 'types/common'
import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'reducers/settings'
import { rgba, getMarketColor } from 'styles/helpers'
import { rgba } from 'styles/helpers'
import Box from 'components/base/Box'
import CounterValue from 'components/CounterValue'
import FormattedVal from 'components/base/FormattedVal'
import OperationDate from './OperationDate'
import ConfirmationCheck from './ConfirmationCheck'
const mapStateToProps = createStructuredSelector({
currencySettings: currencySettingsForAccountSelector,
marketIndicator: marketIndicatorSelector,
})
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
const DATE_COL_SIZE = 100
const ACCOUNT_COL_SIZE = 150
const AMOUNT_COL_SIZE = 150
const CONFIRMATION_COL_SIZE = 44
import ConfirmationCell from './ConfirmationCell'
import DateCell from './DateCell'
import AccountCell from './AccountCell'
import AddressCell from './AddressCell'
import AmountCell from './AmountCell'
const OperationRow = styled(Box).attrs({
horizontal: true,
@ -39,6 +20,7 @@ const OperationRow = styled(Box).attrs({
cursor: pointer;
border-bottom: 1px solid ${p => p.theme.colors.lightGrey};
height: 68px;
opacity: ${p => (p.isOptimistic ? 0.5 : 1)};
&:last-child {
border-bottom: 0;
@ -49,156 +31,38 @@ const OperationRow = styled(Box).attrs({
}
`
const Address = ({ value }: { value: string }) => {
if (!value) {
return <Box />
}
const addrSize = value.length / 2
const left = value.slice(0, 10)
const right = value.slice(-addrSize)
const middle = value.slice(10, -addrSize)
return (
<Box horizontal color="smoke" ff="Open Sans" fontSize={3}>
<div>{left}</div>
<AddressEllipsis>{middle}</AddressEllipsis>
<div>{right}</div>
</Box>
)
}
const AddressEllipsis = styled.div`
display: block;
flex-shrink: 1;
min-width: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const Cell = styled(Box).attrs({
px: 4,
horizontal: true,
alignItems: 'center',
})`
width: ${p => (p.size ? `${p.size}px` : '')};
overflow: ${p => (p.noOverflow ? 'hidden' : '')};
`
type Props = {
operation: Operation,
account: Account,
currencySettings: CurrencySettings,
onOperationClick: ({ operation: Operation, account: Account, marketColor: string }) => void,
marketIndicator: string,
onOperationClick: (operation: Operation, account: Account) => void,
t: T,
op: Operation, // FIXME rename it operation
withAccount: boolean,
}
class OperationComponent extends PureComponent<Props> {
static defaultProps = {
onOperationClick: noop,
withAccount: false,
}
render() {
const {
account,
currencySettings,
onOperationClick,
t,
op,
withAccount,
marketIndicator,
} = this.props
const { unit, currency } = account
const Icon = getCryptoCurrencyIcon(account.currency)
const amount = getOperationAmountNumber(op)
const isNegative = amount < 0
const isOptimistic = op.blockHeight === null
const isConfirmed =
(op.blockHeight ? account.blockHeight - op.blockHeight : 0) > currencySettings.confirmationsNb
const marketColor = getMarketColor({
marketIndicator,
isNegative,
})
// FIXME each cell in a component
onOperationClick = () => {
const { account, onOperationClick, operation } = this.props
onOperationClick(operation, account)
}
render() {
const { account, t, operation, withAccount } = this.props
const isOptimistic = operation.blockHeight === null
return (
<OperationRow
style={{ opacity: isOptimistic ? 0.5 : 1 }}
onClick={() => {
// FIXME why passing down marketColor !? we should retrieve from store / ..
// it should just be onOperationClick(operation)
onOperationClick({ operation: op, account, marketColor })
}}
>
<Cell size={CONFIRMATION_COL_SIZE} align="center" justify="flex-start">
<ConfirmationCheck
type={op.type}
isConfirmed={isConfirmed}
marketColor={marketColor}
t={t}
/>
</Cell>
<Cell size={DATE_COL_SIZE} justifyContent="space-between" px={3}>
<Box>
<Box ff="Open Sans|SemiBold" fontSize={3} color="smoke">
{t(`operationsList:${op.type}`)}
</Box>
<OperationDate date={op.date} />
</Box>
</Cell>
<OperationRow isOptimistic={isOptimistic} onClick={this.onOperationClick}>
<ConfirmationCell operation={operation} account={account} t={t} />
<DateCell operation={operation} t={t} />
{withAccount &&
account && (
<Cell
noOverflow
size={ACCOUNT_COL_SIZE}
horizontal
flow={2}
style={{ cursor: 'pointer' }}
>
<Box
alignItems="center"
justifyContent="center"
style={{ color: account.currency.color }}
>
{Icon && <Icon size={16} />}
</Box>
<Box ff="Open Sans|SemiBold" fontSize={3} color="dark">
{account.name}
</Box>
</Cell>
)}
<Cell grow shrink style={{ display: 'block' }}>
<Address value={op.type === 'IN' ? op.senders[0] : op.recipients[0]} />
</Cell>
<Cell size={AMOUNT_COL_SIZE} justify="flex-end">
<Box alignItems="flex-end">
<FormattedVal
val={amount}
unit={unit}
showCode
fontSize={4}
alwaysShowSign
color={amount < 0 ? 'smoke' : undefined}
/>
<CounterValue
color="grey"
fontSize={3}
date={op.date}
currency={currency}
value={amount}
/>
</Box>
</Cell>
account && <AccountCell accountName={account.name} currency={account.currency} />}
<AddressCell operation={operation} />
<AmountCell operation={operation} currency={account.currency} unit={account.unit} />
</OperationRow>
)
}
}
export default connect(mapStateToProps)(OperationComponent)
export default OperationComponent

31
src/components/OperationsList/SectionTitle.js

@ -0,0 +1,31 @@
// @flow
import React, { PureComponent } from 'react'
import moment from 'moment'
import Box from 'components/base/Box'
const calendarOpts = {
sameDay: 'LL – [Today]',
nextDay: 'LL – [Tomorrow]',
lastDay: 'LL – [Yesterday]',
lastWeek: 'LL',
sameElse: 'LL',
}
type Props = {
day: Date,
}
export class SectionTitle extends PureComponent<Props> {
render() {
const { day } = this.props
const d = moment(day)
return (
<Box ff="Open Sans|SemiBold" fontSize={4} color="grey">
{d.calendar(null, calendarOpts)}
</Box>
)
}
}
export default SectionTitle

77
src/components/OperationsList/index.js

@ -2,7 +2,6 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import moment from 'moment'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { translate } from 'react-i18next'
@ -11,7 +10,7 @@ import {
groupAccountsOperationsByDay,
} from '@ledgerhq/live-common/lib/helpers/account'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { Operation, Account } from '@ledgerhq/live-common/lib/types'
import keyBy from 'lodash/keyBy'
@ -27,15 +26,8 @@ import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text'
import Defer from 'components/base/Defer'
import Operation from './Operation'
const calendarOpts = {
sameDay: 'LL – [Today]',
nextDay: 'LL – [Tomorrow]',
lastDay: 'LL – [Yesterday]',
lastWeek: 'LL',
sameElse: 'LL',
}
import SectionTitle from './SectionTitle'
import OperationC from './Operation'
const ShowMore = styled(Box).attrs({
horizontal: true,
@ -82,7 +74,11 @@ export class OperationsList extends PureComponent<Props, State> {
state = initialState
handleClickOperation = (data: Object) => this.props.openModal(MODAL_OPERATION_DETAILS, data)
handleClickOperation = (operation: Operation, account: Account) =>
this.props.openModal(MODAL_OPERATION_DETAILS, {
operationId: operation.id,
accountId: account.id,
})
// TODO: convert of async/await if fetching with the api
fetchMoreOperations = () => {
@ -93,10 +89,6 @@ export class OperationsList extends PureComponent<Props, State> {
const { account, accounts, canShowMore, t, title, withAccount } = this.props
const { nbToShow } = this.state
const totalOperations = accounts
? accounts.reduce((a, b) => +a + +b.operations.length, 0)
: account.operations.length
if (!account && !accounts) {
console.warn('Preventing render OperationsList because not received account or accounts') // eslint-disable-line no-console
return null
@ -115,36 +107,31 @@ export class OperationsList extends PureComponent<Props, State> {
{title}
</Text>
)}
{groupedOperations.sections.map(group => {
const d = moment(group.day)
return (
<Box flow={2} key={group.day.toISOString()}>
<Box ff="Open Sans|SemiBold" fontSize={4} color="grey">
{d.calendar(null, calendarOpts)}
</Box>
<Card p={0}>
{group.data.map(op => {
const account = accountsMap[op.accountId]
if (!account) {
return null
}
return (
<Operation
account={account}
key={op.id}
onOperationClick={this.handleClickOperation}
op={op}
t={t}
withAccount={withAccount}
/>
)
})}
</Card>
</Box>
)
})}
{groupedOperations.sections.map(group => (
<Box flow={2} key={group.day.toISOString()}>
<SectionTitle day={group.day} />
<Card p={0}>
{group.data.map(operation => {
const account = accountsMap[operation.accountId]
if (!account) {
return null
}
return (
<OperationC
operation={operation}
account={account}
key={operation.id}
onOperationClick={this.handleClickOperation}
t={t}
withAccount={withAccount}
/>
)
})}
</Card>
</Box>
))}
{canShowMore &&
totalOperations > nbToShow && (
!groupedOperations.completed && (
<ShowMore onClick={this.fetchMoreOperations}>
<span>{t('operationsList:showMore')}</span>
<IconAngleDown size={12} />

62
src/components/modals/OperationDetails.js

@ -14,6 +14,7 @@ import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import type { T, CurrencySettings } from 'types/common'
import { MODAL_OPERATION_DETAILS } from 'config/constants'
import { getMarketColor } from 'styles/helpers'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
@ -21,8 +22,9 @@ import Bar from 'components/base/Bar'
import FormattedVal from 'components/base/FormattedVal'
import Modal, { ModalBody, ModalTitle, ModalFooter, ModalContent } from 'components/base/Modal'
import { createStructuredSelector } from 'reselect'
import { currencySettingsForAccountSelector } from 'reducers/settings'
import { createStructuredSelector, createSelector } from 'reselect'
import { accountSelector } from 'reducers/accounts'
import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'reducers/settings'
import CounterValue from 'components/CounterValue'
import ConfirmationCheck from 'components/OperationsList/ConfirmationCheck'
@ -56,25 +58,47 @@ const B = styled(Bar).attrs({
size: 1,
})``
const operationSelector = createSelector(
accountSelector,
(_, { operationId }) => operationId,
(account, operationId) => {
if (!account) return null
const operation = account.operations.find(op => op.id === operationId)
return operation
},
)
const mapStateToProps = createStructuredSelector({
currencySettings: currencySettingsForAccountSelector,
marketIndicator: marketIndicatorSelector,
account: accountSelector,
operation: operationSelector,
currencySettings: createSelector(
state => state,
accountSelector,
(state, account) => (account ? currencySettingsForAccountSelector(state, { account }) : null),
),
})
type Props = {
t: T,
operation: Operation,
account: Account,
operation: ?Operation,
account: ?Account,
currencySettings: ?CurrencySettings,
onClose: () => void,
currencySettings: CurrencySettings,
marketColor: string,
marketIndicator: *,
}
const OperationDetails = connect(mapStateToProps)((props: Props) => {
const { t, onClose, operation, account, marketColor, currencySettings } = props
const { t, onClose, operation, account, currencySettings, marketIndicator } = props
if (!operation || !account || !currencySettings) return null
const { hash, date, senders, recipients, type, fee } = operation
const amount = getOperationAmountNumber(operation)
const { name, unit, currency } = account
const amount = getOperationAmountNumber(operation)
const isNegative = operation.type === 'OUT'
const marketColor = getMarketColor({
marketIndicator,
isNegative,
})
const confirmations = operation.blockHeight ? account.blockHeight - operation.blockHeight : 0
const isConfirmed = confirmations >= currencySettings.confirmationsNb
@ -173,10 +197,8 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => {
type ModalRenderProps = {
data: {
account: Account,
operation: Operation,
type: 'from' | 'to',
marketColor: string,
account: string,
operation: string,
},
onClose: Function,
}
@ -186,17 +208,7 @@ const OperationDetailsWrapper = ({ t }: { t: T }) => (
name={MODAL_OPERATION_DETAILS}
render={(props: ModalRenderProps) => {
const { data, onClose } = props
const { operation, account, type, marketColor } = data
return (
<OperationDetails
t={t}
operation={operation}
account={account}
type={type}
onClose={onClose}
marketColor={marketColor}
/>
)
return <OperationDetails t={t} {...data} onClose={onClose} />
}}
/>
)

Loading…
Cancel
Save