Skip to content

Commit 8be4b74

Browse files
committed
settings: add pubkey/alias/url to the settings page
1 parent 26644b6 commit 8be4b74

File tree

13 files changed

+136
-13
lines changed

13 files changed

+136
-13
lines changed

app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@improbable-eng/grpc-web": "0.12.0",
2323
"big.js": "5.2.2",
2424
"bootstrap": "4.5.0",
25+
"copy-to-clipboard": "3.3.1",
2526
"date-fns": "2.14.0",
2627
"debug": "4.1.1",
2728
"emotion-theming": "10.0.27",

app/src/__stories__/StoryWrapper.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AuthenticationError } from 'util/errors';
66
import { sampleApiResponses } from 'util/tests/sampleData';
77
import { createStore, StoreProvider } from 'store';
88
import { Background } from 'components/base';
9+
import AlertContainer from 'components/common/AlertContainer';
910
import { ThemeProvider } from 'components/theme';
1011

1112
// mock the GRPC client to return sample data instead of making an actual request
@@ -75,6 +76,7 @@ const StoryWrapper: React.FC<{
7576
{store.initialized ? <div style={style}>{children}</div> : null}
7677
</Background>
7778
</Router>
79+
<AlertContainer />
7880
</ThemeProvider>
7981
</StoreProvider>
8082
);

app/src/__tests__/components/settings/SettingsPage.spec.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import React from 'react';
22
import { fireEvent } from '@testing-library/react';
3+
import copyToClipboard from 'copy-to-clipboard';
34
import { renderWithProviders } from 'util/tests';
45
import { createStore, Store } from 'store';
56
import SettingsPage from 'components/settings/SettingsPage';
67

8+
jest.mock('copy-to-clipboard');
9+
710
describe('SettingsPage', () => {
811
let store: Store;
912

10-
beforeEach(() => {
13+
beforeEach(async () => {
1114
store = createStore();
15+
await store.nodeStore.fetchInfo();
1216
store.uiStore.showSettings('');
1317
});
1418

@@ -45,4 +49,44 @@ describe('SettingsPage', () => {
4549
fireEvent.click(getByText('Channel Balance Mode'));
4650
expect(store.router.location.pathname).toEqual('/settings/balance');
4751
});
52+
53+
it('should display the My Node list', () => {
54+
const { getByText } = render();
55+
const { pubkeyLabel, alias, urlLabel } = store.nodeStore;
56+
expect(getByText('My Node')).toBeInTheDocument();
57+
expect(getByText('Pubkey')).toBeInTheDocument();
58+
expect(getByText(pubkeyLabel)).toBeInTheDocument();
59+
expect(getByText('Alias')).toBeInTheDocument();
60+
expect(getByText(alias)).toBeInTheDocument();
61+
expect(getByText('Url')).toBeInTheDocument();
62+
expect(getByText(urlLabel)).toBeInTheDocument();
63+
});
64+
65+
it('should not display the url if it is not defined', () => {
66+
const { queryByText } = render();
67+
expect(queryByText('Url')).toBeInTheDocument();
68+
store.nodeStore.url = '';
69+
expect(queryByText('Url')).not.toBeInTheDocument();
70+
// an invalid url
71+
store.nodeStore.url = 'url-without-at-sign';
72+
expect(queryByText('Url')).not.toBeInTheDocument();
73+
});
74+
75+
it('should copy the pubkey to the clipboard', async () => {
76+
const { getByText } = render();
77+
fireEvent.click(getByText('Pubkey'));
78+
expect(copyToClipboard).toBeCalledWith(store.nodeStore.pubkey);
79+
});
80+
81+
it('should copy the alias to the clipboard', async () => {
82+
const { getByText } = render();
83+
fireEvent.click(getByText('Alias'));
84+
expect(copyToClipboard).toBeCalledWith(store.nodeStore.alias);
85+
});
86+
87+
it('should copy the url to the clipboard', async () => {
88+
const { getByText } = render();
89+
fireEvent.click(getByText('Url'));
90+
expect(copyToClipboard).toBeCalledWith(store.nodeStore.url);
91+
});
4892
});

app/src/assets/icons/copy.svg

Lines changed: 4 additions & 0 deletions
Loading

app/src/components/base/icons.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ReactComponent as ChevronsRightIcon } from 'assets/icons/chevrons-right
88
import { ReactComponent as ChevronsIcon } from 'assets/icons/chevrons.svg';
99
import { ReactComponent as ClockIcon } from 'assets/icons/clock.svg';
1010
import { ReactComponent as CloseIcon } from 'assets/icons/close.svg';
11+
import { ReactComponent as CopyIcon } from 'assets/icons/copy.svg';
1112
import { ReactComponent as DotIcon } from 'assets/icons/dot.svg';
1213
import { ReactComponent as DownloadIcon } from 'assets/icons/download.svg';
1314
import { ReactComponent as MaximizeIcon } from 'assets/icons/maximize.svg';
@@ -70,6 +71,7 @@ export const Chevrons = Icon.withComponent(ChevronsIcon);
7071
export const ChevronsLeft = Icon.withComponent(ChevronsLeftIcon);
7172
export const ChevronsRight = Icon.withComponent(ChevronsRightIcon);
7273
export const Close = Icon.withComponent(CloseIcon);
74+
export const Copy = Icon.withComponent(CopyIcon);
7375
export const Dot = Icon.withComponent(DotIcon);
7476
export const Menu = Icon.withComponent(MenuIcon);
7577
export const Minimize = Icon.withComponent(MinimizeIcon);

app/src/components/common/AlertContainer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ interface AlertToastProps {
6262
const AlertToast: React.FC<AlertToastProps> = ({ alert, onClose }) => {
6363
// use useEffect to only run the side-effect one time
6464
useEffect(() => {
65-
const { id, type, message, title } = alert;
65+
const { id, type, message, title, ms: autoClose } = alert;
6666
// create a component to display inside of the toast
6767
const { Body, Title, Message } = Styled;
6868
const body = (
@@ -72,7 +72,7 @@ const AlertToast: React.FC<AlertToastProps> = ({ alert, onClose }) => {
7272
</Body>
7373
);
7474
// display the toast popup containing the styled body
75-
toast(body, { type, onClose: () => onClose(id) });
75+
toast(body, { type, autoClose, onClose: () => onClose(id) });
7676
}, [alert, onClose]);
7777

7878
// do not render anything to the dom. the toast() func will display the content

app/src/components/settings/GeneralSettings.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@ const Styled = {
1717

1818
const GeneralSettings: React.FC = () => {
1919
const { l } = usePrefixedTranslation('cmps.settings.GeneralSettings');
20-
const { uiStore, settingsStore } = useStore();
20+
const { uiStore, settingsStore, nodeStore } = useStore();
2121

2222
const handleUnit = useCallback(() => uiStore.showSettings('unit'), [uiStore]);
2323
const handleBalance = useCallback(() => uiStore.showSettings('balance'), [uiStore]);
24+
const handleCopyPubkey = useCallback(() => nodeStore.copy('pubkey'), [nodeStore]);
25+
const handleCopyAlias = useCallback(() => nodeStore.copy('alias'), [nodeStore]);
26+
const handleCopyUrl = useCallback(() => nodeStore.copy('url'), [nodeStore]);
2427

2528
const { Wrapper, Content } = Styled;
2629
return (
2730
<Wrapper>
2831
<PageHeader title={l('pageTitle')} />
2932
<Content>
30-
<HeaderFour>{l('title')}</HeaderFour>
33+
<HeaderFour>{l('general')}</HeaderFour>
3134
<SettingItem
3235
name={l('bitcoinUnit')}
3336
value={formatUnit(settingsStore.unit)}
@@ -43,6 +46,29 @@ const GeneralSettings: React.FC = () => {
4346
icon="arrow"
4447
/>
4548
</Content>
49+
<Content>
50+
<HeaderFour>{l('myNode')}</HeaderFour>
51+
<SettingItem
52+
name={l('pubkey')}
53+
value={nodeStore.pubkeyLabel}
54+
onClick={handleCopyPubkey}
55+
icon="copy"
56+
/>
57+
<SettingItem
58+
name={l('alias')}
59+
value={nodeStore.alias}
60+
onClick={handleCopyAlias}
61+
icon="copy"
62+
/>
63+
{nodeStore.urlLabel && (
64+
<SettingItem
65+
name={l('url')}
66+
value={nodeStore.urlLabel}
67+
onClick={handleCopyUrl}
68+
icon="copy"
69+
/>
70+
)}
71+
</Content>
4672
</Wrapper>
4773
);
4874
};

app/src/components/settings/SettingItem.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { ArrowRight, RadioButton } from 'components/base';
2+
import { ArrowRight, Copy, RadioButton } from 'components/base';
33
import { styled } from 'components/theme';
44

55
const Styled = {
@@ -31,7 +31,7 @@ const Styled = {
3131
interface Props {
3232
name: string;
3333
value?: string;
34-
icon: 'arrow' | 'radio';
34+
icon: 'arrow' | 'radio' | 'copy';
3535
checked?: boolean;
3636
onClick: () => void;
3737
}
@@ -46,6 +46,7 @@ const SettingItem: React.FC<Props> = ({ name, value, icon, checked, onClick }) =
4646
<RadioButton role="switch" checked={checked} aria-checked={checked} />
4747
)}
4848
{icon === 'arrow' && <ArrowRight size="large" />}
49+
{icon === 'copy' && <Copy size="large" />}
4950
</Wrapper>
5051
);
5152
};

app/src/i18n/locales/en-US.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@
6666
"cmps.settings.BalanceSettings.pageTitle": "Channel Balance Mode",
6767
"cmps.settings.BalanceSettings.backText": "Settings",
6868
"cmps.settings.BalanceSettings.title": "Optimize For",
69-
"cmps.settings.GeneralSettings.title": "General",
69+
"cmps.settings.GeneralSettings.general": "General",
7070
"cmps.settings.GeneralSettings.bitcoinUnit": "Bitcoin Unit",
7171
"cmps.settings.GeneralSettings.balances": "Channel Balance Mode",
7272
"cmps.settings.GeneralSettings.balancesValue": "Optimize for {{mode}}",
73+
"cmps.settings.GeneralSettings.myNode": "My Node",
74+
"cmps.settings.GeneralSettings.pubkey": "Pubkey",
75+
"cmps.settings.GeneralSettings.alias": "Alias",
76+
"cmps.settings.GeneralSettings.url": "Url",
7377
"cmps.settings.GeneralSettings.pageTitle": "Settings",
7478
"cmps.settings.UnitSettings.pageTitle": "Bitcoin Unit",
7579
"cmps.settings.UnitSettings.backText": "Settings",

app/src/store/stores/nodeStore.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { action, observable, runInAction, toJS } from 'mobx';
1+
import { action, computed, observable, runInAction, toJS } from 'mobx';
22
import { Transaction } from 'types/generated/lnd_pb';
33
import Big from 'big.js';
4+
import copyToClipboard from 'copy-to-clipboard';
45
import debounce from 'lodash/debounce';
6+
import { ellipseInside } from 'util/strings';
57
import { Store } from 'store';
68
import { Wallet } from '../models';
79

@@ -20,6 +22,8 @@ export default class NodeStore {
2022
@observable pubkey = '';
2123
/** the alias of the LND node */
2224
@observable alias = '';
25+
/** the url of the LND node */
26+
@observable url = '';
2327
/** the chain that the LND node is connected to */
2428
@observable chain: NodeChain = 'bitcoin';
2529
/** the network that the LND node is connected to */
@@ -31,6 +35,31 @@ export default class NodeStore {
3135
this._store = store;
3236
}
3337

38+
/** the pubkey shortened to 12 chars with ellipses inside */
39+
@computed get pubkeyLabel() {
40+
return ellipseInside(this.pubkey);
41+
}
42+
43+
/** the url with the pubkey shortened to 12 chars with ellipses inside */
44+
@computed get urlLabel() {
45+
if (!this.url) return '';
46+
47+
const [pubkey, host] = this.url.split('@');
48+
if (!host) return '';
49+
50+
return `${ellipseInside(pubkey)}@${host}`;
51+
}
52+
53+
/**
54+
* Copies the value specified by the key to the user's clipboard
55+
*/
56+
@action.bound
57+
copy(key: 'pubkey' | 'alias' | 'url') {
58+
copyToClipboard(this[key]);
59+
const msg = `Copied ${key} to clipboard`;
60+
this._store.uiStore.notify(msg, '', 'success');
61+
}
62+
3463
/**
3564
* fetch wallet balances from the LND RPC
3665
*/
@@ -46,6 +75,9 @@ export default class NodeStore {
4675
this.chain = info.chainsList[0].chain as NodeChain;
4776
this.network = info.chainsList[0].network as NodeNetwork;
4877
}
78+
if (info.urisList && info.urisList.length > 0) {
79+
this.url = info.urisList[0];
80+
}
4981
this._store.log.info('updated nodeStore info', toJS(this));
5082
});
5183
} catch (error) {

0 commit comments

Comments
 (0)