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.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.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, } `;