Browse Source

Merge pull request #381 from LN-Zap/feature/suggested-channels

Feature/suggested channels
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
4cb157bf39
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      app/api/index.js
  2. 100
      app/components/Contacts/Network.js
  3. 2
      app/components/Contacts/SubmitChannelForm.js
  4. 1
      app/components/Contacts/SubmitChannelForm.scss
  5. 59
      app/components/Contacts/SuggestedNodes.js
  6. 94
      app/components/Contacts/SuggestedNodes.scss
  7. 49
      app/reducers/channels.js
  8. 4
      app/routes/app/components/App.js
  9. 12
      app/routes/app/containers/AppContainer.js
  10. 25
      test/reducers/__snapshots__/channels.spec.js.snap

10
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)
}

100
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 {
</header>
<div className={styles.channels}>
<header className={styles.listHeader}>
<section>
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}>
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span>
</h2>
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}>
{
nonActiveFilters.map(f => (
<li key={f.key} onClick={() => changeFilter(f)}>
{f.name}
</li>
))
}
</ul>
</section>
<section className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}>
{
this.state.refreshing ?
<FaRepeat />
:
'Refresh'
}
</span>
</section>
</header>
{
!loadingChannelPubkeys.length && !currentChannels.length &&
<SuggestedNodes {...suggestedNodesProps} />
}
{
(loadingChannelPubkeys.length > 0 || currentChannels.length) > 0 &&
<header className={styles.listHeader}>
<section>
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}>
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span>
</h2>
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}>
{
nonActiveFilters.map(f => (
<li key={f.key} onClick={() => changeFilter(f)}>
{f.name}
</li>
))
}
</ul>
</section>
<section className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}>
{
this.state.refreshing ?
<FaRepeat />
:
'Refresh'
}
</span>
</section>
</header>
}
<ul className={filterPulldown && styles.fade}>
{
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 {
}
</ul>
</div>
<footer className={styles.search}>
<label htmlFor='search' className={`${styles.label} ${styles.input}`}>
<Isvg src={search} />
</label>
<input
id='search'
type='text'
className={`${styles.text} ${styles.input}`}
placeholder='search by alias or pubkey'
value={searchQuery}
onChange={event => updateChannelSearchQuery(event.target.value)}
/>
</footer>
{
(loadingChannelPubkeys.length > 0 || currentChannels.length) > 0 &&
<footer className={styles.search}>
<label htmlFor='search' className={`${styles.label} ${styles.input}`}>
<Isvg src={search} />
</label>
<input
id='search'
type='text'
className={`${styles.text} ${styles.input}`}
placeholder='search by alias or pubkey'
value={searchQuery}
onChange={event => updateChannelSearchQuery(event.target.value)}
/>
</footer>
}
</div>
)
}
@ -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,

2
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 })

1
app/components/Contacts/SubmitChannelForm.scss

@ -1,6 +1,5 @@
@import '../../variables.scss';
.content {
padding: 0 40px;
font-family: Roboto;

59
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 (
<div className={styles.spinnerContainer}>
<span className={styles.loading}>
<i className={`${styles.spinner} ${styles.closing}`} />
</span>
</div>
)
}
return (
<div className={styles.container}>
<header>
{'Hmmm, looks like you don\'t have any channels yet. Here are some suggested nodes to open a channel with to get started'}
</header>
<ul className={styles.suggestedNodes}>
{
suggestedNodes.map(node => (
<li key={node.pubkey}>
<section>
<span>{node.nickname}</span>
<span>{`${node.pubkey.substring(0, 30)}...`}</span>
</section>
<section>
<span onClick={() => nodeClicked(node)}>Connect</span>
</section>
</li>
))
}
</ul>
</div>
)
}
SuggestedNodes.propTypes = {
suggestedNodesLoading: PropTypes.bool.isRequired,
suggestedNodes: PropTypes.array.isRequired,
setNode: PropTypes.func.isRequired,
openSubmitChannelForm: PropTypes.func.isRequired
}
export default SuggestedNodes

94
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);
}
}

49
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) {

4
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
}

12
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 = {

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

Loading…
Cancel
Save