diff --git a/src/components/CalculateBalance.js b/src/components/CalculateBalance.js index d688c8fc..709f7114 100644 --- a/src/components/CalculateBalance.js +++ b/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 { +class CalculateBalance extends Component { render() { const { children } = this.props return children(this.props) diff --git a/src/components/OperationsList/AccountCell.js b/src/components/OperationsList/AccountCell.js new file mode 100644 index 00000000..1612c921 --- /dev/null +++ b/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 { + render() { + const { currency, accountName } = this.props + const Icon = getCryptoCurrencyIcon(currency) + return ( + + + {Icon && } + + + {accountName} + + + ) + } +} + +export default AccountCell diff --git a/src/components/OperationsList/AddressCell.js b/src/components/OperationsList/AddressCell.js new file mode 100644 index 00000000..e262daf3 --- /dev/null +++ b/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 + } + + 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 ( + +
{left}
+ {middle} +
{right}
+
+ ) +} + +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 { + render() { + const { operation } = this.props + + return ( + +
+ + ) + } +} + +export default AddressCell diff --git a/src/components/OperationsList/AmountCell.js b/src/components/OperationsList/AmountCell.js new file mode 100644 index 00000000..4644fb29 --- /dev/null +++ b/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 { + render() { + const { currency, unit, operation } = this.props + const amount = getOperationAmountNumber(operation) + return ( + + + + + ) + } +} + +export default AmountCell diff --git a/src/components/OperationsList/ConfirmationCell.js b/src/components/OperationsList/ConfirmationCell.js new file mode 100644 index 00000000..8365286d --- /dev/null +++ b/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 { + 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 ( + + + + ) + } +} + +export default connect(mapStateToProps)(ConfirmationCell) diff --git a/src/components/OperationsList/ConfirmationCheck.js b/src/components/OperationsList/ConfirmationCheck.js index 599f8ae1..554a6de6 100644 --- a/src/components/OperationsList/ConfirmationCheck.js +++ b/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 = () => ( - - {type === 'IN' ? : } - {!isConfirmed && ( - - - - )} - - ) +}> { + static defaultProps = { + withTooltip: true, + } - return withTooltip ? ( - - isConfirmed ? t('operationsList:confirmed') : t('operationsList:notConfirmed') - } - > - {renderContent()} - - ) : ( - 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 = ( + + {type === 'IN' ? : } + {!isConfirmed && ( + + + + )} + + ) -ConfirmationCheck.defaultProps = { - withTooltip: true, + return withTooltip ? {content} : content + } } export default ConfirmationCheck diff --git a/src/components/OperationsList/DateCell.js b/src/components/OperationsList/DateCell.js new file mode 100644 index 00000000..fad5efa6 --- /dev/null +++ b/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 { + static defaultProps = { + withAccount: false, + } + + render() { + const { t, operation } = this.props + return ( + + + {t(`operationsList:${operation.type}`)} + + + + ) + } +} + +export default DateCell diff --git a/src/components/OperationsList/Operation.js b/src/components/OperationsList/Operation.js index a60ea26f..c96f7156 100644 --- a/src/components/OperationsList/Operation.js +++ b/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 - } - - const addrSize = value.length / 2 - - const left = value.slice(0, 10) - const right = value.slice(-addrSize) - const middle = value.slice(10, -addrSize) - - return ( - -
{left}
- {middle} -
{right}
-
- ) -} - -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 { 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 ( - { - // FIXME why passing down marketColor !? we should retrieve from store / .. - // it should just be onOperationClick(operation) - onOperationClick({ operation: op, account, marketColor }) - }} - > - - - - - - - {t(`operationsList:${op.type}`)} - - - - + + + {withAccount && - account && ( - - - {Icon && } - - - {account.name} - - - )} - -
- - - - - - - + account && } + + ) } } -export default connect(mapStateToProps)(OperationComponent) +export default OperationComponent diff --git a/src/components/OperationsList/SectionTitle.js b/src/components/OperationsList/SectionTitle.js new file mode 100644 index 00000000..e9a2750c --- /dev/null +++ b/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 { + render() { + const { day } = this.props + const d = moment(day) + return ( + + {d.calendar(null, calendarOpts)} + + ) + } +} + +export default SectionTitle diff --git a/src/components/OperationsList/index.js b/src/components/OperationsList/index.js index 4ddb36eb..37588a87 100644 --- a/src/components/OperationsList/index.js +++ b/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 { 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 { 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 { {title} )} - {groupedOperations.sections.map(group => { - const d = moment(group.day) - return ( - - - {d.calendar(null, calendarOpts)} - - - {group.data.map(op => { - const account = accountsMap[op.accountId] - if (!account) { - return null - } - return ( - - ) - })} - - - ) - })} + {groupedOperations.sections.map(group => ( + + + + {group.data.map(operation => { + const account = accountsMap[operation.accountId] + if (!account) { + return null + } + return ( + + ) + })} + + + ))} {canShowMore && - totalOperations > nbToShow && ( + !groupedOperations.completed && ( {t('operationsList:showMore')} diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index b8b3ce2e..e1c56783 100644 --- a/src/components/modals/OperationDetails.js +++ b/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 ( - - ) + return }} /> )