From f2f972f755ac8390cd808e74937983daf149db2e Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Mon, 18 May 2020 16:04:55 -0400 Subject: [PATCH 01/17] settings: add Unit component to convert sats into other denominations --- app/src/components/common/Unit.tsx | 26 ++++++++++++++++++++ app/src/store/stores/settingsStore.ts | 16 +++++++++++++ app/src/util/constants.ts | 20 ++++++++++++++++ app/src/util/formatters.ts | 34 +++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 app/src/components/common/Unit.tsx create mode 100644 app/src/util/constants.ts create mode 100644 app/src/util/formatters.ts diff --git a/app/src/components/common/Unit.tsx b/app/src/components/common/Unit.tsx new file mode 100644 index 000000000..4001507e1 --- /dev/null +++ b/app/src/components/common/Unit.tsx @@ -0,0 +1,26 @@ +import React, { useMemo } from 'react'; +import { observer } from 'mobx-react-lite'; +import { formatSats } from 'util/formatters'; +import { useStore } from 'store'; + +interface Props { + sats: number; + suffix?: boolean; +} + +const Sats: React.FC = ({ sats, suffix = true }) => { + const { settingsStore } = useStore(); + + // memoize so that formatSats isn't called too often unnecessarily + const text = useMemo(() => { + return formatSats(sats, { + unit: settingsStore.unit, + withSuffix: suffix, + lang: settingsStore.lang, + }); + }, [sats, suffix, settingsStore.unit, settingsStore.lang]); + + return <>{text}; +}; + +export default observer(Sats); diff --git a/app/src/store/stores/settingsStore.ts b/app/src/store/stores/settingsStore.ts index c16e7d28a..56d689816 100644 --- a/app/src/store/stores/settingsStore.ts +++ b/app/src/store/stores/settingsStore.ts @@ -1,11 +1,19 @@ import { action, observable, toJS } from 'mobx'; +import { Unit } from 'util/constants'; import { Store } from 'store'; export default class SettingsStore { private _store: Store; + /** determines if the sidebar nav is visible */ @observable sidebarVisible = true; + /** specifies which denomination to show units in */ + @observable unit: Unit = Unit.sats; + + /** the chosen language */ + @observable lang = 'en-US'; + constructor(store: Store) { this._store = store; } @@ -18,4 +26,12 @@ export default class SettingsStore { this.sidebarVisible = !this.sidebarVisible; this._store.log.info('updated SettingsStore.showSidebar', toJS(this.sidebarVisible)); } + + /** + * sets the unit to display throughout the app + * @param unit the new unit to use + */ + @action.bound setUnit(unit: Unit) { + this.unit = unit; + } } diff --git a/app/src/util/constants.ts b/app/src/util/constants.ts new file mode 100644 index 000000000..5e6cb940a --- /dev/null +++ b/app/src/util/constants.ts @@ -0,0 +1,20 @@ +/** the enumeration of unit supported in the app */ +export enum Unit { + sats = 'sats', + bits = 'bits', + btc = 'btc', +} + +interface UnitFormat { + suffix: string; + name: string; + denominator: number; + decimals: number; +} + +/** a mapping of units to parameters that define how it should be formatted */ +export const Units: { [key in Unit]: UnitFormat } = { + sats: { suffix: 'sats', name: 'Satoshis', denominator: 1, decimals: 0 }, + bits: { suffix: 'bits', name: 'Bits', denominator: 100, decimals: 2 }, + btc: { suffix: 'BTC', name: 'Bitcoin', denominator: 100000000, decimals: 8 }, +}; diff --git a/app/src/util/formatters.ts b/app/src/util/formatters.ts new file mode 100644 index 000000000..71d981859 --- /dev/null +++ b/app/src/util/formatters.ts @@ -0,0 +1,34 @@ +import { Unit, Units } from './constants'; + +interface FormatSatsOptions { + /** the units to convert the sats to (defaults to `sats`) */ + unit?: Unit; + /** if true, return the units abbreviation as a suffix after the amount */ + withSuffix?: boolean; + /** the language to use to determine the format of the number */ + lang?: string; +} + +/** the default values to use for the formatSats function */ +const defaultFormatSatsOptions = { + unit: Unit.sats, + withSuffix: true, + lang: 'en-US', +}; + +/** + * Converts a number representing an amount of satoshis to a string + * @param sats the numeric value in satoshis + * @param options the format options + */ +export const formatSats = (sats: number, options?: FormatSatsOptions) => { + const { unit, withSuffix, lang } = Object.assign(defaultFormatSatsOptions, options); + const { suffix, denominator, decimals } = Units[unit]; + const formatter = Intl.NumberFormat(lang, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); + let text = formatter.format(sats / denominator); + if (withSuffix) text = `${text} ${suffix}`; + return text; +}; From c4b8cf41982d6e5d17c602b7d4c4798fef410257 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Mon, 18 May 2020 16:44:00 -0400 Subject: [PATCH 02/17] settings: update Loop page to use the Unit setting --- app/src/__tests__/components/loop/LoopPage.spec.tsx | 13 +++++-------- app/src/components/common/Tile.tsx | 4 ++-- app/src/components/loop/ChannelRow.tsx | 11 ++++++++--- app/src/components/loop/LoopHistory.tsx | 5 ++++- app/src/components/loop/LoopTiles.tsx | 11 +++-------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/src/__tests__/components/loop/LoopPage.spec.tsx b/app/src/__tests__/components/loop/LoopPage.spec.tsx index 3d894bbf5..953184051 100644 --- a/app/src/__tests__/components/loop/LoopPage.spec.tsx +++ b/app/src/__tests__/components/loop/LoopPage.spec.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { SwapStatus } from 'types/generated/loop_pb'; import { grpc } from '@improbable-eng/grpc-web'; import { fireEvent, waitFor } from '@testing-library/react'; +import { formatSats } from 'util/formatters'; import { renderWithProviders } from 'util/tests'; import { loopListSwaps } from 'util/tests/sampleData'; import { createStore, Store } from 'store'; @@ -37,12 +38,8 @@ describe('LoopPage component', () => { const { getByText, store } = render(); // wait for the channels to be fetched async before checking the UI labels await waitFor(() => expect(store.channelStore.totalInbound).toBeGreaterThan(0)); - expect( - getByText(`${store.channelStore.totalInbound.toLocaleString()} SAT`), - ).toBeInTheDocument(); - expect( - getByText(`${store.channelStore.totalOutbound.toLocaleString()} SAT`), - ).toBeInTheDocument(); + expect(getByText(formatSats(store.channelStore.totalInbound))).toBeInTheDocument(); + expect(getByText(formatSats(store.channelStore.totalOutbound))).toBeInTheDocument(); }); it('should display the loop history records', async () => { @@ -55,9 +52,9 @@ describe('LoopPage component', () => { ); expect(await findByText(formatDate(swap1))).toBeInTheDocument(); - expect(await findByText('530,000 SAT')).toBeInTheDocument(); + expect(await findByText('530,000 sats')).toBeInTheDocument(); expect(await findByText(formatDate(swap2))).toBeInTheDocument(); - expect(await findByText('525,000 SAT')).toBeInTheDocument(); + expect(await findByText('525,000 sats')).toBeInTheDocument(); }); describe('Swap Process', () => { diff --git a/app/src/components/common/Tile.tsx b/app/src/components/common/Tile.tsx index 306e9f0e1..b5f09297d 100644 --- a/app/src/components/common/Tile.tsx +++ b/app/src/components/common/Tile.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { styled } from 'components/theme'; import { Maximize } from './icons'; import { Title } from './text'; @@ -43,7 +43,7 @@ interface Props { * optional text to display in the tile. if this is not * provided, then the `children` will be displayed instead */ - text?: string; + text?: ReactNode; /** * optional click handler for the icon which will not be * visible if this prop is not defined diff --git a/app/src/components/loop/ChannelRow.tsx b/app/src/components/loop/ChannelRow.tsx index 4e5ba46f7..ef00582b2 100644 --- a/app/src/components/loop/ChannelRow.tsx +++ b/app/src/components/loop/ChannelRow.tsx @@ -9,6 +9,7 @@ import Checkbox from 'components/common/Checkbox'; import { Column, Row } from 'components/common/grid'; import StatusDot from 'components/common/StatusDot'; import { Title } from 'components/common/text'; +import Unit from 'components/common/Unit'; import { styled } from 'components/theme'; import ChannelBalance from './ChannelBalance'; @@ -113,15 +114,19 @@ const ChannelRow: React.FC = ({ channel, style }) => { )} - {channel.remoteBalance.toLocaleString()} + - {channel.localBalance.toLocaleString()} + + + {channel.uptimePercent} {ellipseInside(channel.remotePubkey)} - {channel.capacity.toLocaleString()} + + + ); }; diff --git a/app/src/components/loop/LoopHistory.tsx b/app/src/components/loop/LoopHistory.tsx index b1cb79d0e..fd29996e5 100644 --- a/app/src/components/loop/LoopHistory.tsx +++ b/app/src/components/loop/LoopHistory.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite'; import { useStore } from 'store'; import { Column, Row } from 'components/common/grid'; import { SmallText } from 'components/common/text'; +import Unit from 'components/common/Unit'; import { styled } from 'components/theme'; import SwapDot from './SwapDot'; @@ -29,7 +30,9 @@ const LoopHistory: React.FC = () => { {swap.createdOn.toLocaleDateString()} - {`${swap.amount.toLocaleString()} SAT`} + + + ))} diff --git a/app/src/components/loop/LoopTiles.tsx b/app/src/components/loop/LoopTiles.tsx index 243401bb9..41ea4a4ec 100644 --- a/app/src/components/loop/LoopTiles.tsx +++ b/app/src/components/loop/LoopTiles.tsx @@ -4,6 +4,7 @@ import { usePrefixedTranslation } from 'hooks'; import { useStore } from 'store'; import { Column, Row } from 'components/common/grid'; import Tile from 'components/common/Tile'; +import Unit from 'components/common/Unit'; import { styled } from 'components/theme'; import LoopHistory from './LoopHistory'; @@ -27,16 +28,10 @@ const LoopTiles: React.FC = () => { - + } /> - + } /> From 7883bbd9c3902c9756c7be15ef2b6458a780cc50 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Mon, 18 May 2020 16:57:28 -0400 Subject: [PATCH 03/17] settings: update Swap Wizard to use the Unit setting --- .../__tests__/components/common/Range.spec.tsx | 8 ++++---- .../components/loop/SwapWizard.spec.tsx | 17 ++++++++--------- app/src/components/common/Radio.tsx | 4 ++-- app/src/components/common/Range.tsx | 9 ++++++--- app/src/components/loop/swap/SwapReviewStep.tsx | 9 +++++++-- app/src/store/stores/buildSwapStore.ts | 4 +++- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app/src/__tests__/components/common/Range.spec.tsx b/app/src/__tests__/components/common/Range.spec.tsx index 1a37fd5ce..f8451642a 100644 --- a/app/src/__tests__/components/common/Range.spec.tsx +++ b/app/src/__tests__/components/common/Range.spec.tsx @@ -29,7 +29,7 @@ describe('Range component', () => { it('should display slider and value by default', () => { const { getByText, getByLabelText } = render(); expect(getByLabelText('range-slider')).toBeInTheDocument(); - expect(getByText('50 SAT')).toBeInTheDocument(); + expect(getByText('50 sats')).toBeInTheDocument(); }); it('should accept custom props', () => { @@ -41,9 +41,9 @@ describe('Range component', () => { showRadios: true, }); expect(getByLabelText('range-slider')).toBeInTheDocument(); - expect(getByText('5,000 SAT')).toBeInTheDocument(); - expect(getByText('2,500 SAT')).toBeInTheDocument(); - expect(getByText('7,500 SAT')).toBeInTheDocument(); + expect(getByText('5,000 sats')).toBeInTheDocument(); + expect(getByText('2,500 sats')).toBeInTheDocument(); + expect(getByText('7,500 sats')).toBeInTheDocument(); expect(getByText('Min')).toHaveAttribute('aria-checked', 'false'); expect(getByText('Max')).toHaveAttribute('aria-checked', 'false'); }); diff --git a/app/src/__tests__/components/loop/SwapWizard.spec.tsx b/app/src/__tests__/components/loop/SwapWizard.spec.tsx index fe99dbdce..6a0ecc34e 100644 --- a/app/src/__tests__/components/loop/SwapWizard.spec.tsx +++ b/app/src/__tests__/components/loop/SwapWizard.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { SwapDirection } from 'types/state'; import { fireEvent } from '@testing-library/react'; +import { formatSats } from 'util/formatters'; import { renderWithProviders } from 'util/tests'; import { createStore, Store } from 'store'; import SwapWizard from 'components/loop/swap/SwapWizard'; @@ -48,8 +49,8 @@ describe('SwapWizard component', () => { it('should display the correct min an max values', () => { const { getByText } = render(); const { min, max } = store.buildSwapStore.termsForDirection; - expect(getByText(`${min.toLocaleString()} SAT`)).toBeInTheDocument(); - expect(getByText(`${max.toLocaleString()} SAT`)).toBeInTheDocument(); + expect(getByText(formatSats(min))).toBeInTheDocument(); + expect(getByText(formatSats(max))).toBeInTheDocument(); }); it('should display the correct number of channels', () => { @@ -62,10 +63,10 @@ describe('SwapWizard component', () => { const { getByText, getByLabelText } = render(); const build = store.buildSwapStore; expect(build.amount).toEqual(625000); - expect(getByText(`625,000 SAT`)).toBeInTheDocument(); + expect(getByText(`625,000 sats`)).toBeInTheDocument(); fireEvent.change(getByLabelText('range-slider'), { target: { value: '575000' } }); expect(build.amount).toEqual(575000); - expect(getByText(`575,000 SAT`)).toBeInTheDocument(); + expect(getByText(`575,000 sats`)).toBeInTheDocument(); }); }); @@ -88,11 +89,9 @@ describe('SwapWizard component', () => { it('should display the correct values', () => { const { getByText } = render(); const build = store.buildSwapStore; - const toLabel = (x: number) => `${x.toLocaleString()} SAT`; - expect(getByText(toLabel(build.amount))).toBeInTheDocument(); - const pct = ((100 * build.fee) / build.amount).toFixed(2); - expect(getByText(toLabel(build.fee) + ` (${pct}%)`)).toBeInTheDocument(); - expect(getByText(toLabel(build.amount + build.fee))).toBeInTheDocument(); + expect(getByText(formatSats(build.amount))).toBeInTheDocument(); + expect(getByText(build.feesLabel)).toBeInTheDocument(); + expect(getByText(formatSats(build.invoiceTotal))).toBeInTheDocument(); }); }); diff --git a/app/src/components/common/Radio.tsx b/app/src/components/common/Radio.tsx index c79b1aebe..f0241d837 100644 --- a/app/src/components/common/Radio.tsx +++ b/app/src/components/common/Radio.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { styled } from 'components/theme'; import { SmallText } from './text'; @@ -30,7 +30,7 @@ const Styled = { interface Props { text: string; - description?: string; + description?: ReactNode; active?: boolean; right?: boolean; onClick?: () => void; diff --git a/app/src/components/common/Range.tsx b/app/src/components/common/Range.tsx index 13ab87c7f..3ae61157f 100644 --- a/app/src/components/common/Range.tsx +++ b/app/src/components/common/Range.tsx @@ -2,6 +2,7 @@ import React, { ChangeEvent, useCallback } from 'react'; import { styled } from 'components/theme'; import { RangeInput } from './base'; import Radio from './Radio'; +import Unit from './Unit'; const Styled = { RadioGroup: styled.div` @@ -47,13 +48,13 @@ const Range: React.FC = ({ } onClick={handleMinClicked} active={min === value} /> } onClick={handleMaxClicked} active={max === value} right @@ -72,7 +73,9 @@ const Range: React.FC = ({ onChange={handleInputClicked} /> - {value.toLocaleString()} SAT + + + ); }; diff --git a/app/src/components/loop/swap/SwapReviewStep.tsx b/app/src/components/loop/swap/SwapReviewStep.tsx index 35525408c..51ed3df6e 100644 --- a/app/src/components/loop/swap/SwapReviewStep.tsx +++ b/app/src/components/loop/swap/SwapReviewStep.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite'; import { usePrefixedTranslation } from 'hooks'; import { useStore } from 'store'; import { Title, XLargeText } from 'components/common/text'; +import Unit from 'components/common/Unit'; import { styled } from 'components/theme'; import StepButtons from './StepButtons'; import StepSummary from './StepSummary'; @@ -55,7 +56,9 @@ const SwapReviewStep: React.FC = () => {
{l('amount', { type: buildSwapStore.direction })} - {buildSwapStore.amount.toLocaleString()} SAT + + + {l('fees')} @@ -64,7 +67,9 @@ const SwapReviewStep: React.FC = () => { {l('total')} - {buildSwapStore.invoiceTotal.toLocaleString()} SAT + + +
Date: Mon, 18 May 2020 17:09:34 -0400 Subject: [PATCH 04/17] settings: update remaining components to use the Unit setting --- app/src/__stories__/Tile.stories.tsx | 2 +- .../__tests__/components/NodeStatus.spec.tsx | 2 +- .../components/loop/ChannelRow.spec.tsx | 24 +++++++++++-------- app/src/components/NodeStatus.tsx | 12 ++++------ app/src/components/history/HistoryRow.tsx | 5 +++- app/src/components/loop/ChannelRow.tsx | 2 +- .../components/loop/processing/SwapInfo.tsx | 5 +++- 7 files changed, 30 insertions(+), 22 deletions(-) diff --git a/app/src/__stories__/Tile.stories.tsx b/app/src/__stories__/Tile.stories.tsx index 90972515d..f47676e90 100644 --- a/app/src/__stories__/Tile.stories.tsx +++ b/app/src/__stories__/Tile.stories.tsx @@ -23,5 +23,5 @@ export const WithArrowIcon = () => ( ); export const InboundLiquidity = () => ( - + ); diff --git a/app/src/__tests__/components/NodeStatus.spec.tsx b/app/src/__tests__/components/NodeStatus.spec.tsx index 4716e2f74..52463938c 100644 --- a/app/src/__tests__/components/NodeStatus.spec.tsx +++ b/app/src/__tests__/components/NodeStatus.spec.tsx @@ -15,7 +15,7 @@ describe('NodeStatus component', () => { it('should display the lightning balance', () => { const { getByText, store } = render(); store.nodeStore.wallet = { channelBalance: 123, walletBalance: 0 }; - expect(getByText('123 SAT')).toBeInTheDocument(); + expect(getByText('123 sats')).toBeInTheDocument(); }); it('should display the bitcoin balance', () => { diff --git a/app/src/__tests__/components/loop/ChannelRow.spec.tsx b/app/src/__tests__/components/loop/ChannelRow.spec.tsx index 9960e20fa..e71c3a062 100644 --- a/app/src/__tests__/components/loop/ChannelRow.spec.tsx +++ b/app/src/__tests__/components/loop/ChannelRow.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { SwapDirection } from 'types/state'; import { fireEvent } from '@testing-library/react'; +import { formatSats } from 'util/formatters'; import { ellipseInside } from 'util/strings'; import { renderWithProviders } from 'util/tests'; import { createStore, Store } from 'store'; @@ -13,13 +14,6 @@ describe('ChannelRow component', () => { beforeEach(async () => { store = createStore(); - }); - - const render = () => { - return renderWithProviders(, store); - }; - - beforeEach(() => { channel = new Channel({ chanId: '150633093070848', remotePubkey: '02ac59099da6d4bd818e6a81098f5d54580b7c3aa8255c707fa0f95ca89b02cb8c', @@ -32,14 +26,22 @@ describe('ChannelRow component', () => { } as any); }); + const render = () => { + return renderWithProviders(, store); + }; + it('should display the remote balance', () => { const { getByText } = render(); - expect(getByText(channel.remoteBalance.toLocaleString())).toBeInTheDocument(); + expect( + getByText(formatSats(channel.remoteBalance, { withSuffix: false })), + ).toBeInTheDocument(); }); it('should display the local balance', () => { const { getByText } = render(); - expect(getByText(channel.localBalance.toLocaleString())).toBeInTheDocument(); + expect( + getByText(formatSats(channel.localBalance, { withSuffix: false })), + ).toBeInTheDocument(); }); it('should display the uptime', () => { @@ -54,7 +56,9 @@ describe('ChannelRow component', () => { it('should display the capacity', () => { const { getByText } = render(); - expect(getByText(channel.capacity.toLocaleString())).toBeInTheDocument(); + expect( + getByText(formatSats(channel.capacity, { withSuffix: false })), + ).toBeInTheDocument(); }); it('should display correct dot icon for an inactive channel', () => { diff --git a/app/src/components/NodeStatus.tsx b/app/src/components/NodeStatus.tsx index 93127f6cd..983b23969 100644 --- a/app/src/components/NodeStatus.tsx +++ b/app/src/components/NodeStatus.tsx @@ -4,6 +4,7 @@ import { usePrefixedTranslation } from 'hooks'; import { useStore } from 'store'; import { SmallText, Title, XLargeText } from 'components/common/text'; import { Bitcoin, Bolt } from './common/icons'; +import Unit from './common/Unit'; import { styled } from './theme'; const Styled = { @@ -32,23 +33,20 @@ const Styled = { }; const NodeStatus: React.FC = () => { - const { Wrapper, StatusTitle, BoltIcon, BitcoinIcon, Divider } = Styled; - const { l } = usePrefixedTranslation('cmps.NodeStatus'); + const { nodeStore } = useStore(); - const store = useStore(); - const { channelBalance, walletBalance } = store.nodeStore.wallet; - + const { Wrapper, StatusTitle, BoltIcon, BitcoinIcon, Divider } = Styled; return ( {l('title')} - {channelBalance.toLocaleString()} SAT + - {walletBalance.toLocaleString()} + diff --git a/app/src/components/history/HistoryRow.tsx b/app/src/components/history/HistoryRow.tsx index f442fccbc..b3767a65b 100644 --- a/app/src/components/history/HistoryRow.tsx +++ b/app/src/components/history/HistoryRow.tsx @@ -4,6 +4,7 @@ import { usePrefixedTranslation } from 'hooks'; import { Swap } from 'store/models'; import { Column, Row } from 'components/common/grid'; import { Title } from 'components/common/text'; +import Unit from 'components/common/Unit'; import SwapDot from 'components/loop/SwapDot'; import { styled } from 'components/theme'; @@ -75,7 +76,9 @@ const HistoryRow: React.FC = ({ swap, style }) => { {swap.stateLabel} {swap.typeName} - {swap.amount.toLocaleString()} + + + {swap.createdOnLabel} {swap.updatedOnLabel} diff --git a/app/src/components/loop/ChannelRow.tsx b/app/src/components/loop/ChannelRow.tsx index ef00582b2..c61838182 100644 --- a/app/src/components/loop/ChannelRow.tsx +++ b/app/src/components/loop/ChannelRow.tsx @@ -42,7 +42,7 @@ const Styled = { Check: styled(Checkbox)` float: left; margin-top: 18px; - margin-left: 15px; + margin-left: 10px; `, Balance: styled(ChannelBalance)` margin-top: ${ROW_HEIGHT / 2 - 2}px; diff --git a/app/src/components/loop/processing/SwapInfo.tsx b/app/src/components/loop/processing/SwapInfo.tsx index 28b2bb1fc..bf6826c86 100644 --- a/app/src/components/loop/processing/SwapInfo.tsx +++ b/app/src/components/loop/processing/SwapInfo.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { observer } from 'mobx-react-lite'; import { Swap } from 'store/models'; import { Title } from 'components/common/text'; +import Unit from 'components/common/Unit'; import { styled } from 'components/theme'; import SwapDot from '../SwapDot'; @@ -36,7 +37,9 @@ const SwapInfo: React.FC = ({ swap }) => {
{swap.idEllipsed} -
{swap.amount.toLocaleString()} SAT
+
+ +
); From d5b6d3f67dc7819d1c212af0506ef83dfac54ae5 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Mon, 18 May 2020 17:14:10 -0400 Subject: [PATCH 05/17] settings: create Settings page and add link to the nav menu --- app/src/components/Pages.tsx | 3 +++ app/src/components/layout/NavMenu.tsx | 26 ++++++++++++-------- app/src/components/settings/SettingsPage.tsx | 24 ++++++++++++++++++ app/src/i18n/locales/en-US.json | 3 ++- app/src/store/stores/uiStore.ts | 18 ++++++++------ 5 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 app/src/components/settings/SettingsPage.tsx diff --git a/app/src/components/Pages.tsx b/app/src/components/Pages.tsx index b9092b084..820a10e73 100644 --- a/app/src/components/Pages.tsx +++ b/app/src/components/Pages.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite'; import { useStore } from 'store'; import HistoryPage from './history/HistoryPage'; import LoopPage from './loop/LoopPage'; +import SettingsPage from './settings/SettingsPage'; const Pages: React.FC = () => { const { uiStore } = useStore(); @@ -10,6 +11,8 @@ const Pages: React.FC = () => { switch (uiStore.page) { case 'history': return ; + case 'settings': + return ; default: return ; } diff --git a/app/src/components/layout/NavMenu.tsx b/app/src/components/layout/NavMenu.tsx index b9d6d8006..a2082fc89 100644 --- a/app/src/components/layout/NavMenu.tsx +++ b/app/src/components/layout/NavMenu.tsx @@ -44,24 +44,30 @@ const Styled = { `, }; +const NavItem: React.FC<{ page: string; onClick: () => void }> = observer( + ({ page, onClick }) => { + const { l } = usePrefixedTranslation('cmps.layout.NavMenu'); + const { uiStore } = useStore(); + return ( + + {l(page)} + + ); + }, +); + const NavMenu: React.FC = () => { const { l } = usePrefixedTranslation('cmps.layout.NavMenu'); const { uiStore } = useStore(); - const { NavTitle, Nav, NavItem } = Styled; + const { NavTitle, Nav } = Styled; return ( <> {l('menu')} ); diff --git a/app/src/components/settings/SettingsPage.tsx b/app/src/components/settings/SettingsPage.tsx new file mode 100644 index 000000000..b62cb8f53 --- /dev/null +++ b/app/src/components/settings/SettingsPage.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { usePrefixedTranslation } from 'hooks'; +import PageHeader from 'components/common/PageHeader'; +import { styled } from 'components/theme'; + +const Styled = { + Wrapper: styled.div` + padding: 40px 0; + `, +}; + +const HistoryPage: React.FC = () => { + const { l } = usePrefixedTranslation('cmps.settings.SettingsPage'); + + const { Wrapper } = Styled; + return ( + + + + ); +}; + +export default observer(HistoryPage); diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json index fe84bb37b..298d95704 100644 --- a/app/src/i18n/locales/en-US.json +++ b/app/src/i18n/locales/en-US.json @@ -35,5 +35,6 @@ "cmps.layout.NavMenu.loop": "Lightning Loop", "cmps.layout.NavMenu.history": "History", "cmps.layout.NavMenu.settings": "Settings", - "cmps.NodeStatus.title": "Node Status" + "cmps.NodeStatus.title": "Node Status", + "cmps.settings.SettingsPage.pageTitle": "Settings" } diff --git a/app/src/store/stores/uiStore.ts b/app/src/store/stores/uiStore.ts index 3d9be1d91..42a621bc5 100644 --- a/app/src/store/stores/uiStore.ts +++ b/app/src/store/stores/uiStore.ts @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import { Store } from 'store'; -type PageName = 'loop' | 'history'; +type PageName = 'loop' | 'history' | 'settings'; export default class UiStore { private _store: Store; @@ -15,21 +15,25 @@ export default class UiStore { this._store = store; } - /** - * Change to the Loop page - */ + /** Change to the Loop page */ @action.bound goToLoop() { this.page = 'loop'; this._store.log.info('Go to the Loop page'); } - /** - * Change to the History page - */ + /** Change to the History page */ @action.bound goToHistory() { this.page = 'history'; + this._store.log.info('Go to the History page'); + } + + /** Change to the History page */ + @action.bound + goToSettings() { + this.page = 'settings'; + this._store.log.info('Go to the Settings page'); } /** From eb65fc7c61dca013356710d890bcafff6ffd8c98 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Mon, 18 May 2020 22:09:59 -0400 Subject: [PATCH 06/17] settings: add components for General settings --- app/src/components/common/icons.tsx | 4 +- .../components/settings/GeneralSettings.tsx | 48 +++++++++++++++++ app/src/components/settings/SettingItem.tsx | 53 +++++++++++++++++++ app/src/components/settings/SettingsPage.tsx | 25 +++++---- app/src/i18n/locales/en-US.json | 5 +- app/src/store/stores/uiStore.ts | 14 +++-- app/src/util/formatters.ts | 11 ++++ 7 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 app/src/components/settings/GeneralSettings.tsx create mode 100644 app/src/components/settings/SettingItem.tsx diff --git a/app/src/components/common/icons.tsx b/app/src/components/common/icons.tsx index 11c25dc20..8c26ace1b 100644 --- a/app/src/components/common/icons.tsx +++ b/app/src/components/common/icons.tsx @@ -1,9 +1,9 @@ import React, { ReactNode } from 'react'; +import { ReactComponent as ArrowRight } from 'assets/icons/arrow-right.svg'; import { ReactComponent as Clock } from 'assets/icons/clock.svg'; import { ReactComponent as Download } from 'assets/icons/download.svg'; import { styled } from 'components/theme'; -export { ReactComponent as ArrowRight } from 'assets/icons/arrow-right.svg'; export { ReactComponent as ArrowLeft } from 'assets/icons/arrow-left.svg'; export { ReactComponent as Bolt } from 'assets/icons/bolt.svg'; export { ReactComponent as Bitcoin } from 'assets/icons/bitcoin.svg'; @@ -17,11 +17,13 @@ export { ReactComponent as Maximize } from 'assets/icons/maximize.svg'; export { ReactComponent as Refresh } from 'assets/icons/refresh-cw.svg'; interface IconComponents { + 'arrow-right': ReactNode; clock: ReactNode; download: ReactNode; } const components: IconComponents = { + 'arrow-right': , clock: , download: , }; diff --git a/app/src/components/settings/GeneralSettings.tsx b/app/src/components/settings/GeneralSettings.tsx new file mode 100644 index 000000000..0daf70bfb --- /dev/null +++ b/app/src/components/settings/GeneralSettings.tsx @@ -0,0 +1,48 @@ +import React, { useCallback } from 'react'; +import { observer } from 'mobx-react-lite'; +import { usePrefixedTranslation } from 'hooks'; +import { formatUnit } from 'util/formatters'; +import { useStore } from 'store'; +import PageHeader from 'components/common/PageHeader'; +import { Title } from 'components/common/text'; +import { styled } from 'components/theme'; +import SettingItem from './SettingItem'; + +const Styled = { + Wrapper: styled.section``, + Content: styled.div` + margin: 100px 50px; + `, +}; + +const GeneralSettings: React.FC = () => { + const { l } = usePrefixedTranslation('cmps.settings.GeneralSettings'); + const { uiStore, settingsStore } = useStore(); + + const handleUnitClick = useCallback(() => uiStore.showSettings('unit'), [uiStore]); + const handleColorsClick = useCallback(() => uiStore.showSettings('colors'), [uiStore]); + + const { Wrapper, Content } = Styled; + return ( + + + + {l('title')} + + + + + ); +}; + +export default observer(GeneralSettings); diff --git a/app/src/components/settings/SettingItem.tsx b/app/src/components/settings/SettingItem.tsx new file mode 100644 index 000000000..7d3f9dfe5 --- /dev/null +++ b/app/src/components/settings/SettingItem.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { Icon } from 'components/common/icons'; +import { styled } from 'components/theme'; + +const Styled = { + Wrapper: styled.div` + display: flex; + line-height: 80px; + cursor: pointer; + border-bottom: 0.5px solid ${props => props.theme.colors.darkGray}; + + &:last-child { + border-bottom-width: 0; + } + + &:hover { + opacity: 0.8; + } + `, + Name: styled.span` + flex: 1; + font-size: ${props => props.theme.sizes.l}; + `, + Value: styled.span` + color: ${props => props.theme.colors.gray}; + margin-right: 20px; + `, + Icon: styled(Icon)` + line-height: 80px; + `, +}; + +interface Props { + name: string; + value?: string; + arrow?: boolean; + onClick: () => void; +} + +const SettingItem: React.FC = ({ name, value, arrow, children, onClick }) => { + const { Wrapper, Name, Value, Icon } = Styled; + return ( + + {name} + {value && {value}} + {children} + {arrow && } + + ); +}; + +export default observer(SettingItem); diff --git a/app/src/components/settings/SettingsPage.tsx b/app/src/components/settings/SettingsPage.tsx index b62cb8f53..c4e9f4d6a 100644 --- a/app/src/components/settings/SettingsPage.tsx +++ b/app/src/components/settings/SettingsPage.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { observer } from 'mobx-react-lite'; -import { usePrefixedTranslation } from 'hooks'; -import PageHeader from 'components/common/PageHeader'; +import { useStore } from 'store'; import { styled } from 'components/theme'; +import GeneralSettings from './GeneralSettings'; const Styled = { Wrapper: styled.div` @@ -10,15 +10,18 @@ const Styled = { `, }; -const HistoryPage: React.FC = () => { - const { l } = usePrefixedTranslation('cmps.settings.SettingsPage'); +const SettingsPage: React.FC = () => { + const { uiStore } = useStore(); + + let cmp: ReactNode; + switch (uiStore.selectedSetting) { + case 'general': + default: + cmp = ; + } const { Wrapper } = Styled; - return ( - - - - ); + return {cmp}; }; -export default observer(HistoryPage); +export default observer(SettingsPage); diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json index 298d95704..e9d6b36b1 100644 --- a/app/src/i18n/locales/en-US.json +++ b/app/src/i18n/locales/en-US.json @@ -36,5 +36,8 @@ "cmps.layout.NavMenu.history": "History", "cmps.layout.NavMenu.settings": "Settings", "cmps.NodeStatus.title": "Node Status", - "cmps.settings.SettingsPage.pageTitle": "Settings" + "cmps.settings.GeneralSettings.title": "General", + "cmps.settings.GeneralSettings.bitcoinUnit": "Bitcoin Unit", + "cmps.settings.GeneralSettings.balances": "Channel Balances", + "cmps.settings.GeneralSettings.pageTitle": "Settings" } diff --git a/app/src/store/stores/uiStore.ts b/app/src/store/stores/uiStore.ts index 42a621bc5..1beb16946 100644 --- a/app/src/store/stores/uiStore.ts +++ b/app/src/store/stores/uiStore.ts @@ -3,6 +3,8 @@ import { Store } from 'store'; type PageName = 'loop' | 'history' | 'settings'; +type SettingName = 'general' | 'unit' | 'colors'; + export default class UiStore { private _store: Store; @@ -10,6 +12,8 @@ export default class UiStore { @observable page: PageName = 'loop'; /** indicates if the Processing Loops section is displayed on the Loop page */ @observable processingSwapsVisible = false; + /** the selected setting on the Settings page */ + @observable selectedSetting: SettingName = 'general'; constructor(store: Store) { this._store = store; @@ -36,11 +40,15 @@ export default class UiStore { this._store.log.info('Go to the Settings page'); } - /** - * Toggle displaying of the Processing Loops section - */ + /** Toggle displaying of the Processing Loops section */ @action.bound toggleProcessingSwaps() { this.processingSwapsVisible = !this.processingSwapsVisible; } + + /** sets the selected setting to display */ + @action.bound + showSettings(name: SettingName) { + this.selectedSetting = name; + } } diff --git a/app/src/util/formatters.ts b/app/src/util/formatters.ts index 71d981859..220e32b9b 100644 --- a/app/src/util/formatters.ts +++ b/app/src/util/formatters.ts @@ -32,3 +32,14 @@ export const formatSats = (sats: number, options?: FormatSatsOptions) => { if (withSuffix) text = `${text} ${suffix}`; return text; }; + +/** + * Formats a specific unit to display the name and amount in BTC + * ex: Satoshis (0.00000001 BTC) + * @param unit the unit to describe + */ +export const formatUnit = (unit: Unit) => { + const { name, denominator } = Units[unit]; + const btcValue = formatSats(denominator, { unit: Unit.btc }); + return `${name} (${btcValue} BTC)`; +}; From 69f392e3ff1acf51f21ed77a0dcaf1e594a76561 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Mon, 18 May 2020 23:19:27 -0400 Subject: [PATCH 07/17] settings: add Bitcoin Unit settings screen --- app/src/components/common/base.tsx | 17 ++++++ .../components/settings/GeneralSettings.tsx | 4 +- app/src/components/settings/SettingItem.tsx | 15 +++-- app/src/components/settings/SettingsPage.tsx | 13 +++- app/src/components/settings/UnitSettings.tsx | 59 +++++++++++++++++++ app/src/components/theme.tsx | 2 + app/src/i18n/locales/en-US.json | 4 +- 7 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 app/src/components/settings/UnitSettings.tsx diff --git a/app/src/components/common/base.tsx b/app/src/components/common/base.tsx index 6ff593806..b65e1fc42 100644 --- a/app/src/components/common/base.tsx +++ b/app/src/components/common/base.tsx @@ -53,6 +53,23 @@ export const Button = styled.button` } `; +interface RadioButtonProps { + checked?: boolean; +} + +export const RadioButton = styled.span` + display: inline-block; + width: 14px; + height: 14px; + border-radius: 14px; + border: 1px solid ${props => props.theme.colors.lightPurple}; + background-color: ${props => (props.checked ? props.theme.colors.lightPurple : 'none')}; + + &:hover { + opacity: 0.8; + } +`; + /** * the react-virtualized list doesn't play nice with the bootstrap row -15px * margin. We need to manually offset the container and remove the diff --git a/app/src/components/settings/GeneralSettings.tsx b/app/src/components/settings/GeneralSettings.tsx index 0daf70bfb..6966d188b 100644 --- a/app/src/components/settings/GeneralSettings.tsx +++ b/app/src/components/settings/GeneralSettings.tsx @@ -32,13 +32,13 @@ const GeneralSettings: React.FC = () => { name={l('bitcoinUnit')} value={formatUnit(settingsStore.unit)} onClick={handleUnitClick} - arrow + icon="arrow" /> diff --git a/app/src/components/settings/SettingItem.tsx b/app/src/components/settings/SettingItem.tsx index 7d3f9dfe5..ce2e1e6f0 100644 --- a/app/src/components/settings/SettingItem.tsx +++ b/app/src/components/settings/SettingItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { observer } from 'mobx-react-lite'; +import { RadioButton } from 'components/common/base'; import { Icon } from 'components/common/icons'; import { styled } from 'components/theme'; @@ -29,23 +30,27 @@ const Styled = { Icon: styled(Icon)` line-height: 80px; `, + Radio: styled(RadioButton)` + margin-top: 32px; + `, }; interface Props { name: string; value?: string; - arrow?: boolean; + icon: 'arrow' | 'radio'; + checked?: boolean; onClick: () => void; } -const SettingItem: React.FC = ({ name, value, arrow, children, onClick }) => { - const { Wrapper, Name, Value, Icon } = Styled; +const SettingItem: React.FC = ({ name, value, icon, checked, onClick }) => { + const { Wrapper, Name, Value, Radio, Icon } = Styled; return ( {name} {value && {value}} - {children} - {arrow && } + {icon === 'radio' && } + {icon === 'arrow' && } ); }; diff --git a/app/src/components/settings/SettingsPage.tsx b/app/src/components/settings/SettingsPage.tsx index c4e9f4d6a..eb88bf25d 100644 --- a/app/src/components/settings/SettingsPage.tsx +++ b/app/src/components/settings/SettingsPage.tsx @@ -1,8 +1,9 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { useStore } from 'store'; import { styled } from 'components/theme'; import GeneralSettings from './GeneralSettings'; +import UnitSettings from './UnitSettings'; const Styled = { Wrapper: styled.div` @@ -13,8 +14,18 @@ const Styled = { const SettingsPage: React.FC = () => { const { uiStore } = useStore(); + useEffect(() => { + // reset the setting screen to 'general' when this page unmounts + return () => { + uiStore.showSettings('general'); + }; + }, [uiStore]); + let cmp: ReactNode; switch (uiStore.selectedSetting) { + case 'unit': + cmp = ; + break; case 'general': default: cmp = ; diff --git a/app/src/components/settings/UnitSettings.tsx b/app/src/components/settings/UnitSettings.tsx new file mode 100644 index 000000000..6bf3a5301 --- /dev/null +++ b/app/src/components/settings/UnitSettings.tsx @@ -0,0 +1,59 @@ +import React, { useCallback } from 'react'; +import { observer } from 'mobx-react-lite'; +import { usePrefixedTranslation } from 'hooks'; +import { Unit } from 'util/constants'; +import { formatUnit } from 'util/formatters'; +import { useStore } from 'store'; +import PageHeader from 'components/common/PageHeader'; +import { styled } from 'components/theme'; +import SettingItem from './SettingItem'; + +const Styled = { + Wrapper: styled.section``, + Content: styled.div` + margin: 100px auto; + max-width: 500px; + `, +}; + +const UnitItem: React.FC<{ unit: Unit }> = observer(({ unit }) => { + const { settingsStore } = useStore(); + + const handleClick = useCallback(() => { + settingsStore.setUnit(unit); + }, [unit, settingsStore]); + + return ( + + ); +}); + +const UnitSettings: React.FC = () => { + const { l } = usePrefixedTranslation('cmps.settings.UnitSettings'); + const { uiStore } = useStore(); + + const handleBack = useCallback(() => uiStore.showSettings('general'), [uiStore]); + + const { Wrapper, Content } = Styled; + return ( + + + + + + + + + ); +}; + +export default observer(UnitSettings); diff --git a/app/src/components/theme.tsx b/app/src/components/theme.tsx index 390c53dcb..8b105772c 100644 --- a/app/src/components/theme.tsx +++ b/app/src/components/theme.tsx @@ -29,6 +29,7 @@ export interface Theme { orange: string; tileBack: string; purple: string; + lightPurple: string; }; } @@ -58,6 +59,7 @@ const theme: Theme = { orange: '#f66b1c', tileBack: 'rgba(245,245,245,0.04)', purple: '#57038d', + lightPurple: '#a540cd', }, }; diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json index e9d6b36b1..8c6687dd3 100644 --- a/app/src/i18n/locales/en-US.json +++ b/app/src/i18n/locales/en-US.json @@ -39,5 +39,7 @@ "cmps.settings.GeneralSettings.title": "General", "cmps.settings.GeneralSettings.bitcoinUnit": "Bitcoin Unit", "cmps.settings.GeneralSettings.balances": "Channel Balances", - "cmps.settings.GeneralSettings.pageTitle": "Settings" + "cmps.settings.GeneralSettings.pageTitle": "Settings", + "cmps.settings.UnitSettings.pageTitle": "Bitcoin Unit", + "cmps.settings.UnitSettings.backText": "Settings" } From e0481d511efe548307bc7851b350e89df1d215b3 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Tue, 19 May 2020 01:10:07 -0400 Subject: [PATCH 08/17] settings: add channel balance modes to the settings store --- app/src/__tests__/util/balances.spec.ts | 90 +++++++++++++++++++++++++ app/src/store/stores/settingsStore.ts | 13 +++- app/src/util/balances.ts | 35 ++++++++++ app/src/util/constants.ts | 48 +++++++++++++ 4 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 app/src/__tests__/util/balances.spec.ts create mode 100644 app/src/util/balances.ts diff --git a/app/src/__tests__/util/balances.spec.ts b/app/src/__tests__/util/balances.spec.ts new file mode 100644 index 000000000..47d39c62b --- /dev/null +++ b/app/src/__tests__/util/balances.spec.ts @@ -0,0 +1,90 @@ +import { getBalanceStatus } from 'util/balances'; +import { BalanceConfig, BalanceModes, BalanceStatus } from 'util/constants'; + +describe('balances Util', () => { + let config: BalanceConfig; + + describe('Receive Optimized', () => { + beforeEach(() => { + config = BalanceModes.receive; + }); + + it('should return ok status', () => { + expect(getBalanceStatus(100, 1000, config)).toBe(BalanceStatus.ok); + expect(getBalanceStatus(350, 1000, config)).not.toBe(BalanceStatus.ok); + expect(getBalanceStatus(600, 1000, config)).not.toBe(BalanceStatus.ok); + expect(getBalanceStatus(900, 1000, config)).not.toBe(BalanceStatus.ok); + }); + + it('should return warn status', () => { + expect(getBalanceStatus(100, 1000, config)).not.toBe(BalanceStatus.warn); + expect(getBalanceStatus(350, 1000, config)).toBe(BalanceStatus.warn); + expect(getBalanceStatus(600, 1000, config)).toBe(BalanceStatus.warn); + expect(getBalanceStatus(900, 1000, config)).not.toBe(BalanceStatus.warn); + }); + + it('should return danger status', () => { + expect(getBalanceStatus(100, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(350, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(600, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(900, 1000, config)).toBe(BalanceStatus.danger); + }); + }); + + describe('Send Optimized', () => { + beforeEach(() => { + config = BalanceModes.send; + }); + + it('should return ok status', () => { + expect(getBalanceStatus(100, 1000, config)).not.toBe(BalanceStatus.ok); + expect(getBalanceStatus(350, 1000, config)).not.toBe(BalanceStatus.ok); + expect(getBalanceStatus(600, 1000, config)).not.toBe(BalanceStatus.ok); + expect(getBalanceStatus(900, 1000, config)).toBe(BalanceStatus.ok); + }); + + it('should return warn status', () => { + expect(getBalanceStatus(100, 1000, config)).not.toBe(BalanceStatus.warn); + expect(getBalanceStatus(350, 1000, config)).toBe(BalanceStatus.warn); + expect(getBalanceStatus(600, 1000, config)).toBe(BalanceStatus.warn); + expect(getBalanceStatus(900, 1000, config)).not.toBe(BalanceStatus.warn); + }); + + it('should return danger status', () => { + expect(getBalanceStatus(100, 1000, config)).toBe(BalanceStatus.danger); + expect(getBalanceStatus(350, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(600, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(900, 1000, config)).not.toBe(BalanceStatus.danger); + }); + }); + + describe('Routing Optimized', () => { + beforeEach(() => { + config = BalanceModes.routing; + }); + + it('should return ok status', () => { + expect(getBalanceStatus(100, 1000, config)).not.toBe(BalanceStatus.ok); + expect(getBalanceStatus(350, 1000, config)).toBe(BalanceStatus.ok); + expect(getBalanceStatus(600, 1000, config)).toBe(BalanceStatus.ok); + expect(getBalanceStatus(800, 1000, config)).not.toBe(BalanceStatus.ok); + expect(getBalanceStatus(900, 1000, config)).not.toBe(BalanceStatus.ok); + }); + + it('should return warn status', () => { + expect(getBalanceStatus(100, 1000, config)).not.toBe(BalanceStatus.warn); + expect(getBalanceStatus(350, 1000, config)).not.toBe(BalanceStatus.warn); + expect(getBalanceStatus(600, 1000, config)).not.toBe(BalanceStatus.warn); + expect(getBalanceStatus(800, 1000, config)).toBe(BalanceStatus.warn); + expect(getBalanceStatus(900, 1000, config)).not.toBe(BalanceStatus.warn); + }); + + it('should return danger status', () => { + expect(getBalanceStatus(100, 1000, config)).toBe(BalanceStatus.danger); + expect(getBalanceStatus(350, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(600, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(800, 1000, config)).not.toBe(BalanceStatus.danger); + expect(getBalanceStatus(900, 1000, config)).toBe(BalanceStatus.danger); + }); + }); +}); diff --git a/app/src/store/stores/settingsStore.ts b/app/src/store/stores/settingsStore.ts index 56d689816..90e0115ad 100644 --- a/app/src/store/stores/settingsStore.ts +++ b/app/src/store/stores/settingsStore.ts @@ -1,5 +1,5 @@ import { action, observable, toJS } from 'mobx'; -import { Unit } from 'util/constants'; +import { BalanceMode, Unit } from 'util/constants'; import { Store } from 'store'; export default class SettingsStore { @@ -11,6 +11,9 @@ export default class SettingsStore { /** specifies which denomination to show units in */ @observable unit: Unit = Unit.sats; + /** specifies the mode to use to determine channel balance status */ + @observable balanceMode: BalanceMode = BalanceMode.receive; + /** the chosen language */ @observable lang = 'en-US'; @@ -29,9 +32,15 @@ export default class SettingsStore { /** * sets the unit to display throughout the app - * @param unit the new unit to use */ @action.bound setUnit(unit: Unit) { this.unit = unit; } + + /** + * sets the balance mode + */ + @action.bound setBalanceMode(unit: Unit) { + this.unit = unit; + } } diff --git a/app/src/util/balances.ts b/app/src/util/balances.ts new file mode 100644 index 000000000..f1d82a02f --- /dev/null +++ b/app/src/util/balances.ts @@ -0,0 +1,35 @@ +import { BalanceConfig, BalanceConstraint, BalanceStatus } from './constants'; + +/** + * Returns true if the local balance percentage satisfies the constraint + * @param pct the percentage of the local balance + * @param constraint the constraint to check the pct against + */ +const satisfies = (pct: number, constraint: BalanceConstraint) => { + const { min, max, bidirectional } = constraint; + + if (bidirectional && pct < 50) { + pct = 100 - pct; + } + + return min < pct && pct <= max; +}; + +/** + * Returns the current status of the local balance of a channel + * @param local the local balance of the channel + * @param capacity the total capacity of the channel + * @param config the balance configuration (receive, send, routing) + */ +export const getBalanceStatus = ( + local: number, + capacity: number, + config: BalanceConfig, +): BalanceStatus => { + const pct = Math.floor((local * 100) / capacity); + + if (satisfies(pct, config.danger)) return BalanceStatus.danger; + if (satisfies(pct, config.warn)) return BalanceStatus.warn; + + return BalanceStatus.ok; +}; diff --git a/app/src/util/constants.ts b/app/src/util/constants.ts index 5e6cb940a..a4371c43c 100644 --- a/app/src/util/constants.ts +++ b/app/src/util/constants.ts @@ -18,3 +18,51 @@ export const Units: { [key in Unit]: UnitFormat } = { bits: { suffix: 'bits', name: 'Bits', denominator: 100, decimals: 2 }, btc: { suffix: 'BTC', name: 'Bitcoin', denominator: 100000000, decimals: 8 }, }; + +/** the different operating modes of a LN node, used to decide what is considered a good/bad channel balance */ +export enum BalanceMode { + receive = 'receive', + send = 'send', + routing = 'routing', +} + +/** the different balance statuses that a channel may have */ +export enum BalanceStatus { + ok = 'ok', + warn = 'warn', + danger = 'danger', +} + +/** the constraints that need to be satisfied to meet obtain balance status */ +export interface BalanceConstraint { + /** the minimum local balance percentage to satisfy the status */ + min: number; + /** the maximum local balance percentage to satisfy the status */ + max: number; + /** if true, either local or remote balance can satisfy the status */ + bidirectional?: boolean; +} + +/** the balance config params for each status */ +export type BalanceConfig = { + [key in BalanceStatus]: BalanceConstraint; +}; + +/** hard-coded configs for all modes and statuses for channel balances */ +export const BalanceModes: { [key in BalanceMode]: BalanceConfig } = { + receive: { + ok: { min: 0, max: 33 }, + warn: { min: 33, max: 66 }, + danger: { min: 66, max: 100 }, + }, + send: { + ok: { min: 66, max: 100 }, + warn: { min: 33, max: 66 }, + danger: { min: 0, max: 33 }, + }, + routing: { + ok: { min: 50, max: 70, bidirectional: true }, + warn: { min: 70, max: 85, bidirectional: true }, + danger: { min: 85, max: 100, bidirectional: true }, + }, +}; From 0b4b97de978e2702eece8b645cc51938f310467d Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Tue, 19 May 2020 02:17:29 -0400 Subject: [PATCH 09/17] channels: update channel balance colors to be based on the settings mode --- .../__stories__/ChannelBalance.stories.tsx | 17 ++++--- app/src/__stories__/ChannelList.stories.tsx | 38 ++++++++++++---- app/src/__stories__/ChannelRow.stories.tsx | 24 +++++----- .../components/loop/ChannelBalance.spec.tsx | 30 ++++++------- .../components/loop/ChannelRow.spec.tsx | 6 +-- app/src/components/loop/ChannelBalance.tsx | 13 +++--- app/src/components/loop/ChannelRow.tsx | 10 ++--- app/src/components/theme.tsx | 10 +---- app/src/store/models/channel.ts | 45 ++++++++++++------- app/src/store/stores/channelStore.ts | 4 +- app/src/store/stores/settingsStore.ts | 4 +- app/src/types/state.ts | 6 --- app/src/util/balances.ts | 15 +++++++ 13 files changed, 132 insertions(+), 90 deletions(-) diff --git a/app/src/__stories__/ChannelBalance.stories.tsx b/app/src/__stories__/ChannelBalance.stories.tsx index 751692a45..0965fa98b 100644 --- a/app/src/__stories__/ChannelBalance.stories.tsx +++ b/app/src/__stories__/ChannelBalance.stories.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { lndListChannelsOne } from 'util/tests/sampleData'; +import { Store, useStore } from 'store'; import { Channel } from 'store/models'; import ChannelBalance from 'components/loop/ChannelBalance'; @@ -9,27 +10,31 @@ export default { parameters: { centered: true }, }; -const getChannel = (ratio: number) => { - const channel = new Channel(lndListChannelsOne.channelsList[0]); +const getChannel = (store: Store, ratio: number) => { + const channel = new Channel(store, lndListChannelsOne.channelsList[0]); channel.localBalance = channel.capacity * ratio; channel.remoteBalance = channel.capacity * (1 - ratio); return channel; }; export const Good = () => { - return ; + const store = useStore(); + return ; }; export const Warn = () => { - return ; + const store = useStore(); + return ; }; export const Bad = () => { - return ; + const store = useStore(); + return ; }; export const Inactive = () => { - const channel = getChannel(0.45); + const store = useStore(); + const channel = getChannel(store, 0.45); channel.active = false; return ; }; diff --git a/app/src/__stories__/ChannelList.stories.tsx b/app/src/__stories__/ChannelList.stories.tsx index a90a17a5c..306e5547f 100644 --- a/app/src/__stories__/ChannelList.stories.tsx +++ b/app/src/__stories__/ChannelList.stories.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { observable } from 'mobx'; +import { observable, ObservableMap, values } from 'mobx'; +import { BalanceMode } from 'util/constants'; import { useStore } from 'store'; import { Channel } from 'store/models'; import ChannelList from 'components/loop/ChannelList'; @@ -10,19 +11,40 @@ export default { parameters: { contained: true }, }; +const firstTen = (channels: ObservableMap) => { + const ten = values(channels) + .slice(0, 10) + .reduce((result, c) => { + result[c.chanId] = c; + return result; + }, {} as Record); + return observable.map(ten); +}; + export const NoChannels = () => { const store = useStore(); store.channelStore.channels = observable.map(); return ; }; -export const FewChannels = () => { - const store = useStore(); - const channels = store.channelStore.sortedChannels.slice(0, 10).reduce((result, c) => { - result[c.chanId] = c; - return result; - }, {} as Record); - store.channelStore.channels = observable.map(channels); +export const ReceiveMode = () => { + const { channelStore, settingsStore } = useStore(); + settingsStore.balanceMode = BalanceMode.receive; + channelStore.channels = firstTen(channelStore.channels); + return ; +}; + +export const SendMode = () => { + const { channelStore, settingsStore } = useStore(); + settingsStore.balanceMode = BalanceMode.send; + channelStore.channels = firstTen(channelStore.channels); + return ; +}; + +export const RoutingMode = () => { + const { channelStore, settingsStore } = useStore(); + settingsStore.balanceMode = BalanceMode.routing; + channelStore.channels = firstTen(channelStore.channels); return ; }; diff --git a/app/src/__stories__/ChannelRow.stories.tsx b/app/src/__stories__/ChannelRow.stories.tsx index 2685c755b..1bada109c 100644 --- a/app/src/__stories__/ChannelRow.stories.tsx +++ b/app/src/__stories__/ChannelRow.stories.tsx @@ -32,44 +32,48 @@ const renderStory = ( }; export const Good = () => { - const channel = new Channel(lndListChannels.channelsList[0]); - return renderStory(channel, { ratio: 0.59 }); + const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[0]); + return renderStory(channel, { ratio: 0.3 }); }; export const Warn = () => { - const channel = new Channel(lndListChannels.channelsList[1]); - return renderStory(channel, { ratio: 0.28 }); + const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[1]); + return renderStory(channel, { ratio: 0.5 }); }; export const Bad = () => { - const channel = new Channel(lndListChannels.channelsList[2]); + const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[2]); return renderStory(channel, { ratio: 0.91 }); }; export const Inactive = () => { - const channel = new Channel(lndListChannels.channelsList[3]); + const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[3]); channel.active = false; return renderStory(channel); }; export const Editable = () => { - const channel = new Channel(lndListChannels.channelsList[4]); const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[4]); store.buildSwapStore.startSwap(); return renderStory(channel); }; export const Selected = () => { - const channel = new Channel(lndListChannels.channelsList[5]); const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[5]); store.buildSwapStore.startSwap(); store.buildSwapStore.toggleSelectedChannel(channel.chanId); return renderStory(channel); }; export const Disabled = () => { - const channel = new Channel(lndListChannels.channelsList[6]); const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[6]); store.buildSwapStore.startSwap(); store.buildSwapStore.toggleSelectedChannel(channel.chanId); store.buildSwapStore.setDirection(SwapDirection.OUT); @@ -77,8 +81,8 @@ export const Disabled = () => { }; export const Dimmed = () => { - const channel = new Channel(lndListChannels.channelsList[6]); const store = useStore(); + const channel = new Channel(store, lndListChannels.channelsList[6]); store.buildSwapStore.startSwap(); store.buildSwapStore.setDirection(SwapDirection.OUT); return renderStory(channel); diff --git a/app/src/__tests__/components/loop/ChannelBalance.spec.tsx b/app/src/__tests__/components/loop/ChannelBalance.spec.tsx index 0f84d7f2a..24cc2d95f 100644 --- a/app/src/__tests__/components/loop/ChannelBalance.spec.tsx +++ b/app/src/__tests__/components/loop/ChannelBalance.spec.tsx @@ -1,22 +1,24 @@ import React from 'react'; import { renderWithProviders } from 'util/tests'; import { lndListChannelsOne } from 'util/tests/sampleData'; +import { createStore } from 'store'; import { Channel } from 'store/models'; import ChannelBalance from 'components/loop/ChannelBalance'; describe('ChannelBalance component', () => { - const channel: Channel = new Channel(lndListChannelsOne.channelsList[0]); + let channel: Channel; const bgColor = (el: any) => window.getComputedStyle(el).backgroundColor; const width = (el: any) => window.getComputedStyle(el).width; - const shiftBalance = (channel: Channel, ratio: number) => { + const render = (ratio: number, active = true) => { + const store = createStore(); + channel = new Channel(store, lndListChannelsOne.channelsList[0]); channel.localBalance = channel.capacity * ratio; channel.remoteBalance = channel.capacity * (1 - ratio); - }; + channel.active = active; - const render = () => { - const result = renderWithProviders(); + const result = renderWithProviders(, store); const el = result.container.children[0]; return { ...result, @@ -27,27 +29,23 @@ describe('ChannelBalance component', () => { }; it('should display a good balance', () => { - shiftBalance(channel, 0.55); - channel.localBalance = channel.capacity * 0.55; - const { el, remote, local } = render(); + const { el, remote, local } = render(0.25); expect(el.children.length).toBe(3); - expect(width(local)).toBe('55%'); + expect(width(local)).toBe('25%'); expect(bgColor(local)).toBe('rgb(70, 232, 14)'); expect(bgColor(remote)).toBe('rgb(70, 232, 14)'); }); it('should display a warning balance', () => { - shiftBalance(channel, 0.72); - const { el, remote, local } = render(); + const { el, remote, local } = render(0.52); expect(el.children.length).toBe(3); - expect(width(local)).toBe('72%'); + expect(width(local)).toBe('52%'); expect(bgColor(local)).toBe('rgb(246, 107, 28)'); expect(bgColor(remote)).toBe('rgb(246, 107, 28)'); }); it('should display a bad balance', () => { - shiftBalance(channel, 0.93); - const { el, remote, local } = render(); + const { el, remote, local } = render(0.93); expect(el.children.length).toBe(3); expect(width(local)).toBe('93%'); expect(bgColor(local)).toBe('rgb(245, 64, 110)'); @@ -55,9 +53,7 @@ describe('ChannelBalance component', () => { }); it('should display an inactive channel', () => { - channel.active = false; - shiftBalance(channel, 0.55); - const { el, remote, local } = render(); + const { el, remote, local } = render(0.55, false); expect(el.children.length).toBe(3); expect(width(local)).toBe('55%'); expect(bgColor(local)).toBe('rgb(132, 138, 153)'); diff --git a/app/src/__tests__/components/loop/ChannelRow.spec.tsx b/app/src/__tests__/components/loop/ChannelRow.spec.tsx index e71c3a062..ebbc0b29f 100644 --- a/app/src/__tests__/components/loop/ChannelRow.spec.tsx +++ b/app/src/__tests__/components/loop/ChannelRow.spec.tsx @@ -14,7 +14,7 @@ describe('ChannelRow component', () => { beforeEach(async () => { store = createStore(); - channel = new Channel({ + channel = new Channel(store, { chanId: '150633093070848', remotePubkey: '02ac59099da6d4bd818e6a81098f5d54580b7c3aa8255c707fa0f95ca89b02cb8c', capacity: 15000000, @@ -69,8 +69,8 @@ describe('ChannelRow component', () => { }); it.each<[number, string]>([ - [55, 'success'], - [75, 'warn'], + [20, 'success'], + [50, 'warn'], [90, 'error'], ])('should display correct dot icon for a "%s" balance', (localPct, label) => { channel.localBalance = channel.capacity * (localPct / 100); diff --git a/app/src/components/loop/ChannelBalance.tsx b/app/src/components/loop/ChannelBalance.tsx index 0611af25e..917fa3fe5 100644 --- a/app/src/components/loop/ChannelBalance.tsx +++ b/app/src/components/loop/ChannelBalance.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { observer } from 'mobx-react-lite'; -import { BalanceLevel } from 'types/state'; +import { statusToColor } from 'util/balances'; +import { BalanceStatus } from 'util/constants'; import { Channel } from 'store/models'; -import { levelToColor, styled } from 'components/theme'; +import { styled } from 'components/theme'; const Styled = { - Wrapper: styled.div<{ pct: number; level: BalanceLevel; active: boolean }>` + Wrapper: styled.div<{ pct: number; status: BalanceStatus; active: boolean }>` display: flex; width: 100%; @@ -15,13 +16,13 @@ const Styled = { &:first-of-type { flex-grow: 1; background-color: ${props => - levelToColor(props.level, props.active, props.theme)}; + statusToColor(props.status, props.active, props.theme)}; } &:last-of-type { width: ${props => props.pct}%; background-color: ${props => - levelToColor(props.level, props.active, props.theme)}; + statusToColor(props.status, props.active, props.theme)}; } } `, @@ -45,7 +46,7 @@ const ChannelBalance: React.FC = ({ channel, className }) => { return ( diff --git a/app/src/components/loop/ChannelRow.tsx b/app/src/components/loop/ChannelRow.tsx index c61838182..01c599f4c 100644 --- a/app/src/components/loop/ChannelRow.tsx +++ b/app/src/components/loop/ChannelRow.tsx @@ -1,7 +1,7 @@ import React, { CSSProperties } from 'react'; import { observer } from 'mobx-react-lite'; -import { BalanceLevel } from 'types/state'; import { usePrefixedTranslation } from 'hooks'; +import { BalanceStatus } from 'util/constants'; import { ellipseInside } from 'util/strings'; import { useStore } from 'store'; import { Channel } from 'store/models'; @@ -76,12 +76,12 @@ export const ChannelRowHeader: React.FC = () => { const ChannelDot: React.FC<{ channel: Channel }> = observer(({ channel }) => { if (!channel.active) return ; - switch (channel.balanceLevel) { - case BalanceLevel.good: + switch (channel.balanceStatus) { + case BalanceStatus.ok: return ; - case BalanceLevel.warn: + case BalanceStatus.warn: return ; - case BalanceLevel.bad: + case BalanceStatus.danger: return ; } }); diff --git a/app/src/components/theme.tsx b/app/src/components/theme.tsx index 8b105772c..3beea2954 100644 --- a/app/src/components/theme.tsx +++ b/app/src/components/theme.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { BalanceLevel } from 'types/state'; + import emotionStyled, { CreateStyled } from '@emotion/styled/macro'; import { ThemeProvider as EmotionThemeProvider } from 'emotion-theming'; @@ -63,14 +63,6 @@ const theme: Theme = { }, }; -export const levelToColor = (level: BalanceLevel, active: boolean, theme: Theme) => { - if (!active) return theme.colors.gray; - - if (level === BalanceLevel.bad) return theme.colors.pink; - if (level === BalanceLevel.warn) return theme.colors.orange; - return theme.colors.green; -}; - export const styled = emotionStyled as CreateStyled; export const ThemeProvider: React.FC = ({ children }) => { diff --git a/app/src/store/models/channel.ts b/app/src/store/models/channel.ts index a08283a69..3d0441c91 100644 --- a/app/src/store/models/channel.ts +++ b/app/src/store/models/channel.ts @@ -1,8 +1,12 @@ import { action, computed, observable } from 'mobx'; import * as LND from 'types/generated/lnd_pb'; -import { BalanceLevel } from 'types/state'; +import { getBalanceStatus } from 'util/balances'; +import { BalanceMode, BalanceModes } from 'util/constants'; +import { Store } from 'store/store'; export default class Channel { + private _store: Store; + @observable chanId = ''; @observable remotePubkey = ''; @observable capacity = 0; @@ -12,7 +16,8 @@ export default class Channel { @observable uptime = 0; @observable lifetime = 0; - constructor(lndChannel: LND.Channel.AsObject) { + constructor(store: Store, lndChannel: LND.Channel.AsObject) { + this._store = store; this.update(lndChannel); } @@ -28,28 +33,36 @@ export default class Channel { * remote balances */ @computed get localPercent() { - return Math.round( - (this.localBalance * 100) / (this.localBalance + this.remoteBalance), - ); + return Math.round((this.localBalance * 100) / this.capacity); } /** - * The imbalance percentage irregardless of direction + * The order to sort this channel based on the current mode */ - @computed get balancePercent() { - const pct = this.localPercent; - return pct >= 50 ? pct : 100 - pct; + @computed get sortOrder() { + const mode = this._store.settingsStore.balanceMode; + switch (mode) { + case BalanceMode.receive: + // the highest local percentage first + return this.localPercent; + case BalanceMode.send: + // the lowest local percentage first + return 100 - this.localPercent; + case BalanceMode.routing: + const pct = this.localPercent; + // disregard direction. the highest local percentage first + return pct >= 50 ? pct : 100 - pct; + default: + return 0; + } } /** - * Determines the balance level of a channel based on the percentage on each side + * The balance status of this channel (ok, warn, or danger) */ - @computed get balanceLevel() { - const pct = this.balancePercent; - - if (pct > 85) return BalanceLevel.bad; - if (pct > 65) return BalanceLevel.warn; - return BalanceLevel.good; + @computed get balanceStatus() { + const mode = this._store.settingsStore.balanceMode; + return getBalanceStatus(this.localBalance, this.capacity, BalanceModes[mode]); } /** diff --git a/app/src/store/stores/channelStore.ts b/app/src/store/stores/channelStore.ts index 2e72e38fa..c5d947585 100644 --- a/app/src/store/stores/channelStore.ts +++ b/app/src/store/stores/channelStore.ts @@ -26,7 +26,7 @@ export default class ChannelStore { @computed get sortedChannels() { return values(this.channels) .slice() - .sort((a, b) => b.balancePercent - a.balancePercent); + .sort((a, b) => b.sortOrder - a.sortOrder); } /** @@ -61,7 +61,7 @@ export default class ChannelStore { if (existing) { existing.update(lndChan); } else { - this.channels.set(lndChan.chanId, new Channel(lndChan)); + this.channels.set(lndChan.chanId, new Channel(this._store, lndChan)); } }); // remove any channels in state that are not in the API response diff --git a/app/src/store/stores/settingsStore.ts b/app/src/store/stores/settingsStore.ts index 90e0115ad..f036f5d41 100644 --- a/app/src/store/stores/settingsStore.ts +++ b/app/src/store/stores/settingsStore.ts @@ -40,7 +40,7 @@ export default class SettingsStore { /** * sets the balance mode */ - @action.bound setBalanceMode(unit: Unit) { - this.unit = unit; + @action.bound setBalanceMode(mode: BalanceMode) { + this.balanceMode = mode; } } diff --git a/app/src/types/state.ts b/app/src/types/state.ts index 8b586b06a..b7d16ff78 100644 --- a/app/src/types/state.ts +++ b/app/src/types/state.ts @@ -1,9 +1,3 @@ -export enum BalanceLevel { - good = 'good', - warn = 'warn', - bad = 'bad', -} - export enum SwapDirection { IN = 'Loop In', OUT = 'Loop Out', diff --git a/app/src/util/balances.ts b/app/src/util/balances.ts index f1d82a02f..4200fa524 100644 --- a/app/src/util/balances.ts +++ b/app/src/util/balances.ts @@ -1,3 +1,4 @@ +import { Theme } from 'components/theme'; import { BalanceConfig, BalanceConstraint, BalanceStatus } from './constants'; /** @@ -33,3 +34,17 @@ export const getBalanceStatus = ( return BalanceStatus.ok; }; + +/** + * Converts a channel balance status to a theme color + * @param level the status of the channel + * @param active whether the channel is active or not + * @param theme the app theme containing colors + */ +export const statusToColor = (level: BalanceStatus, active: boolean, theme: Theme) => { + if (!active) return theme.colors.gray; + + if (level === BalanceStatus.danger) return theme.colors.pink; + if (level === BalanceStatus.warn) return theme.colors.orange; + return theme.colors.green; +}; From f3c29cfc1bdfd93c26bcbdb4d2f7d41ae26e0cc1 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Tue, 19 May 2020 02:42:44 -0400 Subject: [PATCH 10/17] settings: add Balance Mode settings screen --- .../components/settings/BalanceSettings.tsx | 61 +++++++++++++++++++ .../components/settings/GeneralSettings.tsx | 11 ++-- app/src/components/settings/SettingsPage.tsx | 13 ++-- app/src/i18n/locales/en-US.json | 9 ++- app/src/store/stores/uiStore.ts | 3 +- 5 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 app/src/components/settings/BalanceSettings.tsx diff --git a/app/src/components/settings/BalanceSettings.tsx b/app/src/components/settings/BalanceSettings.tsx new file mode 100644 index 000000000..5f9233ef7 --- /dev/null +++ b/app/src/components/settings/BalanceSettings.tsx @@ -0,0 +1,61 @@ +import React, { useCallback } from 'react'; +import { observer } from 'mobx-react-lite'; +import { usePrefixedTranslation } from 'hooks'; +import { BalanceMode } from 'util/constants'; +import { useStore } from 'store'; +import PageHeader from 'components/common/PageHeader'; +import { Title } from 'components/common/text'; +import { styled } from 'components/theme'; +import SettingItem from './SettingItem'; + +const Styled = { + Wrapper: styled.section``, + Content: styled.div` + margin: 100px auto; + max-width: 500px; + `, +}; + +const BalanceModeItem: React.FC<{ mode: BalanceMode }> = observer(({ mode }) => { + const { l } = usePrefixedTranslation('enums.BalanceMode'); + const { settingsStore } = useStore(); + + const handleClick = useCallback(() => { + settingsStore.setBalanceMode(mode); + }, [mode, settingsStore]); + + return ( + + ); +}); + +const BalanceSettings: React.FC = () => { + const { l } = usePrefixedTranslation('cmps.settings.BalanceSettings'); + const { uiStore } = useStore(); + + const handleBack = useCallback(() => uiStore.showSettings('general'), [uiStore]); + + const { Wrapper, Content } = Styled; + return ( + + + + {l('title')} + + + + + + ); +}; + +export default observer(BalanceSettings); diff --git a/app/src/components/settings/GeneralSettings.tsx b/app/src/components/settings/GeneralSettings.tsx index 6966d188b..a7101821d 100644 --- a/app/src/components/settings/GeneralSettings.tsx +++ b/app/src/components/settings/GeneralSettings.tsx @@ -17,10 +17,11 @@ const Styled = { const GeneralSettings: React.FC = () => { const { l } = usePrefixedTranslation('cmps.settings.GeneralSettings'); + const { l: lbm } = usePrefixedTranslation('enums.BalanceMode'); const { uiStore, settingsStore } = useStore(); - const handleUnitClick = useCallback(() => uiStore.showSettings('unit'), [uiStore]); - const handleColorsClick = useCallback(() => uiStore.showSettings('colors'), [uiStore]); + const handleUnit = useCallback(() => uiStore.showSettings('unit'), [uiStore]); + const handleBalance = useCallback(() => uiStore.showSettings('balance'), [uiStore]); const { Wrapper, Content } = Styled; return ( @@ -31,13 +32,13 @@ const GeneralSettings: React.FC = () => { diff --git a/app/src/components/settings/SettingsPage.tsx b/app/src/components/settings/SettingsPage.tsx index eb88bf25d..ccfb0c00c 100644 --- a/app/src/components/settings/SettingsPage.tsx +++ b/app/src/components/settings/SettingsPage.tsx @@ -1,7 +1,8 @@ -import React, { ReactNode, useEffect } from 'react'; +import React, { ReactNode } from 'react'; import { observer } from 'mobx-react-lite'; import { useStore } from 'store'; import { styled } from 'components/theme'; +import BalanceSettings from './BalanceSettings'; import GeneralSettings from './GeneralSettings'; import UnitSettings from './UnitSettings'; @@ -14,18 +15,14 @@ const Styled = { const SettingsPage: React.FC = () => { const { uiStore } = useStore(); - useEffect(() => { - // reset the setting screen to 'general' when this page unmounts - return () => { - uiStore.showSettings('general'); - }; - }, [uiStore]); - let cmp: ReactNode; switch (uiStore.selectedSetting) { case 'unit': cmp = ; break; + case 'balance': + cmp = ; + break; case 'general': default: cmp = ; diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json index 8c6687dd3..0852734c1 100644 --- a/app/src/i18n/locales/en-US.json +++ b/app/src/i18n/locales/en-US.json @@ -1,4 +1,7 @@ { + "enums.BalanceMode.receive": "Receiving", + "enums.BalanceMode.send": "Sending", + "enums.BalanceMode.routing": "Routing", "cmps.history.HistoryPage.backText": "Lightning Loop", "cmps.history.HistoryPage.pageTitle": "Loop History", "cmps.history.HistoryRowHeader.status": "Status", @@ -36,9 +39,13 @@ "cmps.layout.NavMenu.history": "History", "cmps.layout.NavMenu.settings": "Settings", "cmps.NodeStatus.title": "Node Status", + "cmps.settings.BalanceSettings.pageTitle": "Channel Balance Mode", + "cmps.settings.BalanceSettings.backText": "Settings", + "cmps.settings.BalanceSettings.title": "Optimize For", "cmps.settings.GeneralSettings.title": "General", "cmps.settings.GeneralSettings.bitcoinUnit": "Bitcoin Unit", - "cmps.settings.GeneralSettings.balances": "Channel Balances", + "cmps.settings.GeneralSettings.balances": "Channel Balance Mode", + "cmps.settings.GeneralSettings.balancesValue": "Optimize for {{mode}}", "cmps.settings.GeneralSettings.pageTitle": "Settings", "cmps.settings.UnitSettings.pageTitle": "Bitcoin Unit", "cmps.settings.UnitSettings.backText": "Settings" diff --git a/app/src/store/stores/uiStore.ts b/app/src/store/stores/uiStore.ts index 1beb16946..7a31d1dbe 100644 --- a/app/src/store/stores/uiStore.ts +++ b/app/src/store/stores/uiStore.ts @@ -3,7 +3,7 @@ import { Store } from 'store'; type PageName = 'loop' | 'history' | 'settings'; -type SettingName = 'general' | 'unit' | 'colors'; +type SettingName = 'general' | 'unit' | 'balance'; export default class UiStore { private _store: Store; @@ -37,6 +37,7 @@ export default class UiStore { @action.bound goToSettings() { this.page = 'settings'; + this.selectedSetting = 'general'; this._store.log.info('Go to the Settings page'); } From 127dfaafee0fb0a90517d7358f94ee4fd984c999 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Tue, 19 May 2020 03:24:41 -0400 Subject: [PATCH 11/17] swaps: infer swap direction based on selected channels --- app/src/components/common/base.tsx | 2 +- app/src/components/loop/LoopActions.tsx | 18 ++++++++---------- app/src/store/stores/buildSwapStore.ts | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/src/components/common/base.tsx b/app/src/components/common/base.tsx index b65e1fc42..52cc76a80 100644 --- a/app/src/components/common/base.tsx +++ b/app/src/components/common/base.tsx @@ -37,7 +37,7 @@ export const Button = styled.button` color: ${props => props.theme.colors.white}; background-color: ${props => props.ghost ? 'transparent' : props.theme.colors.tileBack}; - border-width: ${props => (props.borderless ? '0' : '1px')}; + border-width: ${props => (props.borderless && !props.primary ? '0' : '1px')}; border-color: ${props => props.primary ? props.theme.colors.green : props.theme.colors.white}; border-style: solid; diff --git a/app/src/components/loop/LoopActions.tsx b/app/src/components/loop/LoopActions.tsx index 8a95aff1b..ed115b1f1 100644 --- a/app/src/components/loop/LoopActions.tsx +++ b/app/src/components/loop/LoopActions.tsx @@ -34,13 +34,11 @@ const Styled = { const LoopActions: React.FC = () => { const { l } = usePrefixedTranslation('cmps.loop.LoopActions'); const { buildSwapStore } = useStore(); - const handleLoopOut = useCallback( - () => buildSwapStore.setDirection(SwapDirection.OUT), - [buildSwapStore], - ); - const handleLoopIn = useCallback(() => buildSwapStore.setDirection(SwapDirection.IN), [ - buildSwapStore, + const { setDirection, inferredDirection } = buildSwapStore; + const handleLoopOut = useCallback(() => setDirection(SwapDirection.OUT), [ + setDirection, ]); + const handleLoopIn = useCallback(() => setDirection(SwapDirection.IN), [setDirection]); const selectedCount = buildSwapStore.selectedChanIds.length; const { Wrapper, Actions, CloseIcon, Selected } = Styled; @@ -52,16 +50,16 @@ const LoopActions: React.FC = () => { {selectedCount} {l('channelsSelected')}