diff --git a/app/api/index.js b/app/api/index.js
index 7ea44624..43544a56 100644
--- a/app/api/index.js
+++ b/app/api/index.js
@@ -24,3 +24,13 @@ export function requestBlockHeight() {
.then(response => response.data)
.catch(error => error)
}
+
+export function requestSuggestedNodes() {
+ const BASE_URL = 'http://zap.jackmallers.com/suggested-peers'
+ return axios({
+ method: 'get',
+ url: BASE_URL
+ })
+ .then(response => response.data)
+ .catch(error => error)
+}
diff --git a/app/components/Contacts/Network.js b/app/components/Contacts/Network.js
index 77e64e6d..32990562 100644
--- a/app/components/Contacts/Network.js
+++ b/app/components/Contacts/Network.js
@@ -8,6 +8,7 @@ import plus from 'icons/plus.svg'
import search from 'icons/search.svg'
import Value from 'components/Value'
+import SuggestedNodes from './SuggestedNodes'
import styles from './Network.scss'
@@ -48,9 +49,10 @@ class Network extends Component {
setSelectedChannel,
- closeChannel
- } = this.props
+ closeChannel,
+ suggestedNodesProps
+ } = this.props
const refreshClicked = () => {
// turn the spinner on
@@ -139,37 +141,44 @@ class Network extends Component {
-
-
-
- {filter.name}
-
-
- {
- nonActiveFilters.map(f => (
- - changeFilter(f)}>
- {f.name}
-
- ))
- }
-
-
-
-
- { this.repeat = ref }}>
- {
- this.state.refreshing ?
-
- :
- 'Refresh'
- }
-
-
-
+ {
+ !loadingChannelPubkeys.length && !currentChannels.length &&
+
+ }
+
+ {
+ (loadingChannelPubkeys.length > 0 || currentChannels.length) > 0 &&
+
+
+
+ {filter.name}
+
+
+ {
+ nonActiveFilters.map(f => (
+ - changeFilter(f)}>
+ {f.name}
+
+ ))
+ }
+
+
+
+ { this.repeat = ref }}>
+ {
+ this.state.refreshing ?
+
+ :
+ 'Refresh'
+ }
+
+
+
+ }
{
- loadingChannelPubkeys.map((loadingPubkey) => {
+ loadingChannelPubkeys.length > 0 && loadingChannelPubkeys.map((loadingPubkey) => {
// TODO(jimmymow): refactor this out. same logic is in displayNodeName above
const node = find(nodes, n => loadingPubkey === n.pub_key)
const nodeDisplay = () => {
@@ -264,20 +273,22 @@ class Network extends Component {
}
-
-
+ {
+ (loadingChannelPubkeys.length > 0 || currentChannels.length) > 0 &&
+
+ }
)
}
@@ -292,6 +303,7 @@ Network.propTypes = {
balance: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
ticker: PropTypes.object.isRequired,
+ suggestedNodesProps: PropTypes.object.isRequired,
fetchChannels: PropTypes.func.isRequired,
openContactsForm: PropTypes.func.isRequired,
diff --git a/app/components/Contacts/SubmitChannelForm.js b/app/components/Contacts/SubmitChannelForm.js
index bd77e6d7..8aedd15d 100644
--- a/app/components/Contacts/SubmitChannelForm.js
+++ b/app/components/Contacts/SubmitChannelForm.js
@@ -38,7 +38,7 @@ class SubmitChannelForm extends React.Component {
const formSubmitted = () => {
// dont submit to LND if they havent set channel capacity amount
- if (contactCapacity > 0) { return }
+ if (contactCapacity > 0) { console.log('hello?'); return }
// submit the channel to LND
openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: contactCapacity })
diff --git a/app/components/Contacts/SubmitChannelForm.scss b/app/components/Contacts/SubmitChannelForm.scss
index d580f763..84b543a9 100644
--- a/app/components/Contacts/SubmitChannelForm.scss
+++ b/app/components/Contacts/SubmitChannelForm.scss
@@ -1,6 +1,5 @@
@import '../../variables.scss';
-
.content {
padding: 0 40px;
font-family: Roboto;
diff --git a/app/components/Contacts/SuggestedNodes.js b/app/components/Contacts/SuggestedNodes.js
new file mode 100644
index 00000000..9025c976
--- /dev/null
+++ b/app/components/Contacts/SuggestedNodes.js
@@ -0,0 +1,59 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import styles from './SuggestedNodes.scss'
+
+const SuggestedNodes = ({
+ suggestedNodesLoading,
+ suggestedNodes,
+ setNode,
+ openSubmitChannelForm
+}) => {
+ const nodeClicked = (n) => {
+ // set the node public key for the submit form
+ setNode({ pub_key: n.pubkey, addresses: [{ addr: n.host }] })
+ // open the submit form
+ openSubmitChannelForm()
+ }
+ if (suggestedNodesLoading) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ {'Hmmm, looks like you don\'t have any channels yet. Here are some suggested nodes to open a channel with to get started'}
+
+
+
+ {
+ suggestedNodes.map(node => (
+ -
+
+ {node.nickname}
+ {`${node.pubkey.substring(0, 30)}...`}
+
+
+ nodeClicked(node)}>Connect
+
+
+ ))
+ }
+
+
+ )
+}
+
+SuggestedNodes.propTypes = {
+ suggestedNodesLoading: PropTypes.bool.isRequired,
+ suggestedNodes: PropTypes.array.isRequired,
+ setNode: PropTypes.func.isRequired,
+ openSubmitChannelForm: PropTypes.func.isRequired
+}
+
+export default SuggestedNodes
diff --git a/app/components/Contacts/SuggestedNodes.scss b/app/components/Contacts/SuggestedNodes.scss
new file mode 100644
index 00000000..2fb330a2
--- /dev/null
+++ b/app/components/Contacts/SuggestedNodes.scss
@@ -0,0 +1,94 @@
+@import '../../variables.scss';
+
+.container {
+ color: $white;
+ padding: 10px;
+
+ header {
+ font-size: 12px;
+ line-height: 16px;
+ }
+
+ .suggestedNodes {
+ margin-top: 30px;
+
+ li {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin: 10px 0;
+ padding: 10px 0;
+
+ section span {
+ font-size: 12px;
+ }
+
+ section:nth-child(1) {
+ span {
+ display: block;
+
+ &:nth-child(2) {
+ font-size: 10px;
+ margin-top: 5px;
+ }
+ }
+ }
+
+ section:nth-child(2) {
+ span {
+ font-size: 10px;
+ opacity: 0.5;
+ cursor: pointer;
+ transition: all 0.25s;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+ }
+ }
+}
+
+.spinnerContainer {
+ text-align: center;
+}
+
+.spinner {
+ height: 25px;
+ width: 25px;
+ border: 1px solid rgba(235, 184, 100, 0.1);
+ border-left-color: rgba(235, 184, 100, 0.4);
+ -webkit-border-radius: 999px;
+ -moz-border-radius: 999px;
+ border-radius: 999px;
+ -webkit-animation: animation-rotate 1000ms linear infinite;
+ -moz-animation: animation-rotate 1000ms linear infinite;
+ -o-animation: animation-rotate 1000ms linear infinite;
+ animation: animation-rotate 1000ms linear infinite;
+ display: inline-block;
+}
+
+@-webkit-keyframes animation-rotate {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+
+@-moz-keyframes animation-rotate {
+ 100% {
+ -moz-transform: rotate(360deg);
+ }
+}
+
+@-o-keyframes animation-rotate {
+ 100% {
+ -o-transform: rotate(360deg);
+ }
+}
+
+@keyframes animation-rotate {
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/app/reducers/channels.js b/app/reducers/channels.js
index 779a0bf9..9c019fd0 100644
--- a/app/reducers/channels.js
+++ b/app/reducers/channels.js
@@ -3,6 +3,7 @@ import { ipcRenderer } from 'electron'
import filter from 'lodash/filter'
import { btc } from 'utils'
import { showNotification } from 'notifications'
+import { requestSuggestedNodes } from '../api'
import { setError } from './error'
// ------------------------------------
// Constants
@@ -40,6 +41,9 @@ export const CLOSE_CONTACT_MODAL = 'CLOSE_CONTACT_MODAL'
export const SET_SELECTED_CHANNEL = 'SET_SELECTED_CHANNEL'
+export const GET_SUGGESTED_NODES = 'GET_SUGGESTED_NODES'
+export const RECEIVE_SUGGESTED_NODES = 'RECEIVE_SUGGESTED_NODES'
+
// ------------------------------------
// Actions
// ------------------------------------
@@ -150,6 +154,26 @@ export function setSelectedChannel(selectedChannel) {
}
}
+export function getSuggestedNodes() {
+ return {
+ type: GET_SUGGESTED_NODES
+ }
+}
+
+export function receiveSuggestedNodes(suggestedNodes) {
+ return {
+ type: RECEIVE_SUGGESTED_NODES,
+ suggestedNodes
+ }
+}
+
+export const fetchSuggestedNodes = () => async (dispatch) => {
+ dispatch(getSuggestedNodes())
+ const suggestedNodes = await requestSuggestedNodes()
+
+ dispatch(receiveSuggestedNodes(suggestedNodes))
+}
+
// Send IPC event for peers
export const fetchChannels = () => async (dispatch) => {
dispatch(getChannels())
@@ -344,7 +368,10 @@ const ACTION_HANDLERS = {
[OPEN_CONTACT_MODAL]: (state, { channel }) => ({ ...state, contactModal: { isOpen: true, channel } }),
[CLOSE_CONTACT_MODAL]: state => ({ ...state, contactModal: { isOpen: false, channel: null } }),
- [SET_SELECTED_CHANNEL]: (state, { selectedChannel }) => ({ ...state, selectedChannel })
+ [SET_SELECTED_CHANNEL]: (state, { selectedChannel }) => ({ ...state, selectedChannel }),
+
+ [GET_SUGGESTED_NODES]: state => ({ ...state, suggestedNodesLoading: true }),
+ [RECEIVE_SUGGESTED_NODES]: (state, { suggestedNodes }) => ({ ...state, suggestedNodesLoading: false, suggestedNodes })
}
const channelsSelectors = {}
@@ -534,7 +561,25 @@ const initialState = {
channel: null
},
- selectedChannel: null
+ selectedChannel: null,
+
+ // nodes stored at zap.jackmallers.com/suggested-peers manages by JimmyMow
+ // we store this node list here and if the user doesnt have any channels
+ // we show them this list in case they wanna use our suggestions to connect
+ // to the network and get started
+ // **** Example ****
+ // {
+ // pubkey: "02212d3ec887188b284dbb7b2e6eb40629a6e14fb049673f22d2a0aa05f902090e",
+ // host: "testnet-lnd.yalls.org",
+ // nickname: "Yalls",
+ // description: "Top up prepaid mobile phones with bitcoin and altcoins in USA and around the world"
+ // }
+ // ****
+ suggestedNodes: {
+ mainnet: [],
+ testnet: []
+ },
+ suggestedNodesLoading: false
}
export default function channelsReducer(state = initialState, action) {
diff --git a/app/routes/app/components/App.js b/app/routes/app/components/App.js
index 870ee71c..7936c287 100644
--- a/app/routes/app/components/App.js
+++ b/app/routes/app/components/App.js
@@ -24,6 +24,7 @@ class App extends Component {
fetchInfo,
newAddress,
fetchChannels,
+ fetchSuggestedNodes,
fetchBalance,
fetchDescribeNetwork
} = this.props
@@ -36,6 +37,8 @@ class App extends Component {
newAddress('np2wkh')
// fetch nodes channels
fetchChannels()
+ // fetch suggested nodes list from zap.jackmallers.com/suggested-peers
+ fetchSuggestedNodes()
// fetch nodes balance
fetchBalance()
// fetch LN network from nides POV
@@ -126,6 +129,7 @@ App.propTypes = {
fetchChannels: PropTypes.func.isRequired,
fetchBalance: PropTypes.func.isRequired,
fetchDescribeNetwork: PropTypes.func.isRequired,
+ fetchSuggestedNodes: PropTypes.func.isRequired,
children: PropTypes.object.isRequired
}
diff --git a/app/routes/app/containers/AppContainer.js b/app/routes/app/containers/AppContainer.js
index cad23e15..bcaf929e 100644
--- a/app/routes/app/containers/AppContainer.js
+++ b/app/routes/app/containers/AppContainer.js
@@ -27,6 +27,7 @@ import { fetchBlockHeight, lndSelectors } from 'reducers/lnd'
import {
fetchChannels,
+ fetchSuggestedNodes,
openChannel,
closeChannel,
channelsSelectors,
@@ -105,6 +106,7 @@ const mapDispatchToProps = {
fetchBalance,
fetchChannels,
+ fetchSuggestedNodes,
openChannel,
closeChannel,
toggleFilterPulldown,
@@ -312,7 +314,15 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
changeFilter: dispatchProps.changeFilter,
updateChannelSearchQuery: dispatchProps.updateChannelSearchQuery,
setSelectedChannel: dispatchProps.setSelectedChannel,
- closeChannel: dispatchProps.closeChannel
+ closeChannel: dispatchProps.closeChannel,
+
+ suggestedNodesProps: {
+ suggestedNodesLoading: stateProps.channels.suggestedNodesLoading,
+ suggestedNodes: stateProps.info.data.testnet ? stateProps.channels.suggestedNodes.testnet : stateProps.channels.suggestedNodes.mainnet,
+
+ setNode: dispatchProps.setNode,
+ openSubmitChannelForm: () => dispatchProps.setChannelFormType('SUBMIT_CHANNEL_FORM')
+ }
}
const contactsFormProps = {
diff --git a/test/reducers/__snapshots__/channels.spec.js.snap b/test/reducers/__snapshots__/channels.spec.js.snap
index 518f65af..d1e95f6b 100644
--- a/test/reducers/__snapshots__/channels.spec.js.snap
+++ b/test/reducers/__snapshots__/channels.spec.js.snap
@@ -54,6 +54,11 @@ Object {
},
"searchQuery": "",
"selectedChannel": null,
+ "suggestedNodes": Object {
+ "mainnet": Array [],
+ "testnet": Array [],
+ },
+ "suggestedNodesLoading": false,
"viewType": 0,
}
`;
@@ -112,6 +117,11 @@ Object {
},
"searchQuery": "",
"selectedChannel": null,
+ "suggestedNodes": Object {
+ "mainnet": Array [],
+ "testnet": Array [],
+ },
+ "suggestedNodesLoading": false,
"viewType": 0,
}
`;
@@ -171,6 +181,11 @@ Object {
],
"searchQuery": "",
"selectedChannel": null,
+ "suggestedNodes": Object {
+ "mainnet": Array [],
+ "testnet": Array [],
+ },
+ "suggestedNodesLoading": false,
"viewType": 0,
}
`;
@@ -229,6 +244,11 @@ Object {
},
"searchQuery": "",
"selectedChannel": null,
+ "suggestedNodes": Object {
+ "mainnet": Array [],
+ "testnet": Array [],
+ },
+ "suggestedNodesLoading": false,
"viewType": 0,
}
`;
@@ -287,6 +307,11 @@ Object {
},
"searchQuery": "",
"selectedChannel": null,
+ "suggestedNodes": Object {
+ "mainnet": Array [],
+ "testnet": Array [],
+ },
+ "suggestedNodesLoading": false,
"viewType": 0,
}
`;