Browse Source

Merge pull request #359 from LN-Zap/feature/new-network-form

Feature/new network form
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
e485d05e82
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .flowconfig
  2. 160
      app/components/Contacts/AddChannel.js
  3. 141
      app/components/Contacts/AddChannel.scss
  4. 40
      app/components/Contacts/ChannelForm.js
  5. 29
      app/components/Contacts/ChannelForm.scss
  6. 99
      app/components/Contacts/ConnectManually.js
  7. 139
      app/components/Contacts/ConnectManually.scss
  8. 35
      app/components/Contacts/Network.js
  9. 51
      app/components/Contacts/Network.scss
  10. 121
      app/components/Contacts/SubmitChannelForm.js
  11. 150
      app/components/Contacts/SubmitChannelForm.scss
  12. 2
      app/components/Form/Form.js
  13. 7
      app/components/Form/Form.scss
  14. 4
      app/main.dev.js
  15. 130
      app/reducers/channelform.js
  16. 5
      app/reducers/channels.js
  17. 127
      app/reducers/contactsform.js
  18. 2
      app/reducers/index.js
  19. 14
      app/routes/app/components/App.js
  20. 2
      app/routes/app/components/App.scss
  21. 89
      app/routes/app/containers/AppContainer.js
  22. 57
      test/reducers/__snapshots__/channels.spec.js.snap
  23. 4
      test/reducers/channels.spec.js

1
.flowconfig

@ -13,6 +13,7 @@
[include]
[libs]
flow-typed
[options]
esproposal.class_static_fields=enable

160
app/components/Contacts/AddChannel.js

@ -0,0 +1,160 @@
import React from 'react'
import PropTypes from 'prop-types'
import Isvg from 'react-inlinesvg'
import x from 'icons/x.svg'
import styles from './AddChannel.scss'
const AddChannel = ({
contactsform,
closeContactsForm,
openSubmitChannelForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
setNode,
activeChannelPubkeys,
nonActiveChannelPubkeys,
pendingOpenChannelPubkeys,
filteredNetworkNodes,
loadingChannelPubkeys,
showManualForm,
openManualForm
}) => {
const renderRightSide = (node) => {
if (loadingChannelPubkeys.includes(node.pub_key)) {
return (
<span className={styles.inactive}>
<div className={styles.loading}>
<div className={styles.spinner} />
</div>
</span>
)
}
if (activeChannelPubkeys.includes(node.pub_key)) {
return (
<span className={`${styles.online} ${styles.inactive}`}>
<span>Online</span>
</span>
)
}
if (nonActiveChannelPubkeys.includes(node.pub_key)) {
return (
<span className={`${styles.offline} ${styles.inactive}`}>
<span>Offline</span>
</span>
)
}
if (pendingOpenChannelPubkeys.includes(node.pub_key)) {
return (
<span className={`${styles.pending} ${styles.inactive}`}>
<span>Pending</span>
</span>
)
}
if (!node.addresses.length) {
return (
<span className={`${styles.private} ${styles.inactive}`}>
Private
</span>
)
}
return (
<span
className={styles.connect}
onClick={() => {
// set the node public key for the submit form
setNode(node)
// open the submit form
openSubmitChannelForm()
}}
>
Connect
</span>
)
}
const searchUpdated = (search) => {
updateContactFormSearchQuery(search)
if (search.includes('@') && search.split('@')[0].length === 66) {
updateManualFormSearchQuery(search)
}
}
return (
<div className={styles.container}>
<header className={styles.header}>
<input
type='text'
placeholder='Search the network...'
className={styles.searchInput}
value={contactsform.searchQuery}
onChange={event => searchUpdated(event.target.value)}
// ref={input => input && input.focus()}
/>
<span onClick={closeContactsForm} className={styles.closeIcon}>
<Isvg src={x} />
</span>
</header>
<section className={styles.nodes}>
<ul className={styles.networkResults}>
{
filteredNetworkNodes.map(node => (
<li key={node.pub_key}>
<section>
{
node.alias.length > 0 ?
<h2>
<span>{node.alias.trim()}</span>
<span>({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})</span>
</h2>
:
<h2>
<span>{node.pub_key}</span>
</h2>
}
</section>
<section>
{renderRightSide(node)}
</section>
</li>
))
}
</ul>
</section>
{
showManualForm &&
<section className={styles.manualForm}>
<p>Hm, looks like we can&apos;t see that node from here, wanna try to manually connect?</p>
<div className={styles.manualConnectButton} onClick={openManualForm}>Connect Manually</div>
</section>
}
</div>
)
}
AddChannel.propTypes = {
contactsform: PropTypes.object.isRequired,
closeContactsForm: PropTypes.func.isRequired,
openSubmitChannelForm: PropTypes.func.isRequired,
updateContactFormSearchQuery: PropTypes.func.isRequired,
updateManualFormSearchQuery: PropTypes.func.isRequired,
setNode: PropTypes.func.isRequired,
activeChannelPubkeys: PropTypes.array.isRequired,
nonActiveChannelPubkeys: PropTypes.array.isRequired,
pendingOpenChannelPubkeys: PropTypes.array.isRequired,
filteredNetworkNodes: PropTypes.array.isRequired,
loadingChannelPubkeys: PropTypes.array.isRequired,
showManualForm: PropTypes.bool.isRequired,
openManualForm: PropTypes.func.isRequired
}
export default AddChannel

141
app/components/Contacts/AddChannel.scss

@ -0,0 +1,141 @@
@import '../../variables.scss';
.container {
position: relative;
width: 30%;
display: inline-block;
vertical-align: top;
height: 100vh;
background: #31343f;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
background: linear-gradient(270deg, #868B9F 0%, #333C5E 100%);
padding: 15px 10px;
color: $white;
input {
background: transparent;
outline: 0;
border: 0;
color: $white;
font-size: 14px;
width: 90%;
}
.closeIcon {
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
svg {
height: 14px;
width: 14px;
}
}
}
.nodes {
background: #31343F;
.networkResults {
overflow-y: auto;
margin-top: 30px;
padding: 0 10px;
color: $white;
li {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 10px 0;
h2 {
font-size: 10px;
font-weight: bold;
letter-spacing: 1.3px;
span {
display: inline-block;
vertical-align: middle;
&:nth-child(1) {
font-size: 10px;
font-weight: bold;
letter-spacing: 1.3px;
}
&:nth-child(2) {
display: block;
color: $darkestgrey;
font-size: 8px;
margin-top: 5px;
}
}
}
.connect {
cursor: pointer;
color: $darkestgrey;
transition: all 0.25s;
font-size: 10px;
&:hover {
color: darken($darkestgrey, 10%);
}
}
.inactive {
font-size: 10px;
display: inline-block;
vertical-align: top;
&.online {
color: $green;
}
&.offline {
color: $darkestgrey;
}
&.pending {
color: $orange;
}
&.private {
color: darken($darkestgrey, 50%);
}
}
}
}
}
.manualForm {
color: $white;
text-align: center;
margin: 0 25px;
p {
font-size: 14px;
margin: 20px 0;
}
div {
background: #383B47;
font-size: 16px;
padding: 10px;
cursor: pointer;
transition: all 0.25s;
&:hover {
background: lighten(#383B47, 10%);
}
}
}

40
app/components/Contacts/ChannelForm.js

@ -0,0 +1,40 @@
import React from 'react'
import PropTypes from 'prop-types'
import Isvg from 'react-inlinesvg'
import x from 'icons/x.svg'
import ConnectManually from './ConnectManually'
import SubmitChannelForm from './SubmitChannelForm'
import styles from './ChannelForm.scss'
const FORM_TYPES = {
MANUAL_FORM: ConnectManually,
SUBMIT_CHANNEL_FORM: SubmitChannelForm
}
const ChannelForm = ({ formType, formProps, closeForm }) => {
if (!formType) { return null }
const FormComponent = FORM_TYPES[formType]
return (
<div className={styles.container}>
<div className={styles.closeContainer}>
<span onClick={closeForm}>
<Isvg src={x} />
</span>
</div>
<FormComponent {...formProps} />
</div>
)
}
ChannelForm.propTypes = {
formType: PropTypes.string,
formProps: PropTypes.object.isRequired,
closeForm: PropTypes.func.isRequired
}
export default ChannelForm

29
app/components/Contacts/ChannelForm.scss

@ -0,0 +1,29 @@
@import '../../variables.scss';
.container {
position: absolute;
top: 0;
z-index: 10;
height: 100vh;
width: 100%;
background: #31343F;
}
.closeContainer {
text-align: right;
padding: 20px 40px 0px;
span {
cursor: pointer;
opacity: 1.0;
transition: 0.25s all;
&:hover {
opacity: 0.5;
}
}
svg {
color: $white;
}
}

99
app/components/Contacts/ConnectManually.js

@ -0,0 +1,99 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './ConnectManually.scss'
class ConnectManually extends React.Component {
render() {
const {
manualSearchQuery,
manualFormIsValid,
updateManualFormErrors,
openSubmitChannelForm,
updateManualFormSearchQuery,
setNode,
showErrors
} = this.props
const formSubmitted = () => {
if (!manualFormIsValid.isValid) {
updateManualFormErrors(manualFormIsValid.errors)
return
}
// clear any existing errors
updateManualFormErrors({ manualInput: null })
const [pub_key, addr] = manualSearchQuery && manualSearchQuery.split('@')
// the SubmitChannel component is expecting a node object that looks like the following
// {
// pub_key: 'some_string',
// addresses: [
// {
// addr: 'some_host_address'
// }
// ]
// }
// knowing this we will set the node object with the known format and plug in the pubkey + host accordingly
setNode({ pub_key, addresses: [{ addr }] })
// now we close the ConnectManually form and open the SubmitChannel form by chaning the channelFormType
openSubmitChannelForm()
}
return (
<div className={styles.content}>
<header className={styles.header}>
<h1>Connect Manually</h1>
<p>Please enter the peer&apos;s pubkey@host</p>
</header>
<section className={styles.peer}>
<div className={styles.input}>
<input
type='text'
placeholder='pubkey@host'
value={manualSearchQuery}
onChange={event => updateManualFormSearchQuery(event.target.value)}
/>
</div>
</section>
<section className={`${styles.errorMessage} ${showErrors.manualInput && styles.active}`}>
{showErrors.manualInput &&
<span>{manualFormIsValid && manualFormIsValid.errors.manualInput}</span>
}
</section>
<section className={styles.submit}>
<div
className={`${styles.button} ${manualFormIsValid.isValid && styles.active}`}
onClick={formSubmitted}
>
Submit
</div>
</section>
</div>
)
}
}
ConnectManually.propTypes = {
manualSearchQuery: PropTypes.string.isRequired,
manualFormIsValid: PropTypes.object.isRequired,
updateManualFormErrors: PropTypes.func.isRequired,
openSubmitChannelForm: PropTypes.func.isRequired,
updateManualFormSearchQuery: PropTypes.func.isRequired,
setNode: PropTypes.func.isRequired,
showErrors: PropTypes.object.isRequired
}
export default ConnectManually

139
app/components/Contacts/ConnectManually.scss

@ -0,0 +1,139 @@
@import '../../variables.scss';
.container {
position: absolute;
top: 0;
z-index: 10;
height: 100vh;
width: 100%;
background: #31343F;
}
.closeContainer {
text-align: right;
padding: 20px 40px 0px;
span {
cursor: pointer;
opacity: 1.0;
transition: 0.25s all;
&:hover {
opacity: 0.5;
}
}
svg {
color: $white;
}
}
.content {
padding: 0 40px;
font-family: Roboto;
color: $white;
.header {
padding: 20px 100px;
h1 {
margin-bottom: 15px;
font-size: 20px;
}
p {
text-align: center;
line-height: 1.3;
font-size: 14px;
}
}
}
.header {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid $spaceborder;
h1 {
font-size: 22px;
font-weight: 100;
margin-top: 10px;
letter-spacing: 1.5px;
}
}
.title {
margin: 50px 0;
h2 {
font-size: 14px;
background: $spaceblue;
padding: 10px;
border-radius: 17.5px;
display: inline;
}
}
.input {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 50px;
input {
font-size: 20px;
}
}
.input input {
background: transparent;
outline: none;
border: 0;
color: $gold;
-webkit-text-fill-color: $white;
width: 100%;
font-weight: 200;
}
.input input::-webkit-input-placeholder, ::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.errorMessage {
margin-top: 20px;
font-size: 12px;
color: $red;
min-height: 12px;
visibility: hidden;
&.active {
visibility: visible;
}
}
.submit {
margin-top: 50px;
text-align: center;
.button {
width: 235px;
margin: 0 auto;
padding: 20px 10px;
background: #31343f;
opacity: 0.5;
cursor: pointer;
transition: 0.25s all;
&.active {
background: $gold;
opacity: 1.0;
&:hover {
background: darken($gold, 5%);
}
}
}
}

35
app/components/Contacts/Network.js

@ -6,6 +6,7 @@ import { FaAngleDown, FaCircle, FaRepeat } from 'react-icons/lib/fa'
import { btc } from 'utils'
import plus from 'icons/plus.svg'
import search from 'icons/search.svg'
import styles from './Network.scss'
class Network extends Component {
@ -22,8 +23,8 @@ class Network extends Component {
channels: {
searchQuery,
filterPulldown,
filter
// loadingChannelPubkeys,
filter,
loadingChannelPubkeys
// closingChannelIds
},
currentChannels,
@ -81,10 +82,16 @@ class Network extends Component {
}
const channelStatus = (channel) => {
// if the channel has a confirmation_height property that means it's pending
if (Object.prototype.hasOwnProperty.call(channel, 'confirmation_height')) { return 'pending' }
// if the channel has a closing tx that means it's closing
if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { return 'closing' }
// if the channel isn't active that means the remote peer isn't online
if (!channel.active) { return 'offline' }
// if all of the above conditionals fail we can assume the node is online :)
return 'online'
}
@ -100,7 +107,9 @@ class Network extends Component {
</span>
</section>
<section className={`${styles.addChannel} hint--bottom-left`} onClick={openContactsForm} data-hint='Open a channel'>
<Isvg src={plus} />
<span className={styles.plusContainer}>
<Isvg src={plus} />
</span>
</section>
</header>
@ -134,6 +143,26 @@ class Network extends Component {
</header>
<ul className={filterPulldown && styles.fade}>
{
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 = () => {
if (node && node.alias.length) { return node.alias }
return loadingPubkey.substring(0, 10)
}
return (
<li key={loadingPubkey} className={styles.channel}>
<span>{nodeDisplay()}</span>
<span className={`${styles.loading} hint--left`} data-hint='loading'>
<i className={styles.spinner} />
</span>
</li>
)
})
}
{
currentChannels.length > 0 && currentChannels.map((channelObj, index) => {
const channel = Object.prototype.hasOwnProperty.call(channelObj, 'channel') ? channelObj.channel : channelObj

51
app/components/Contacts/Network.scss

@ -2,7 +2,7 @@
.network {
position: relative;
width: 20%;
width: 30%;
display: inline-block;
vertical-align: top;
height: 100vh;
@ -33,8 +33,16 @@
cursor: pointer;
transition: all 0.25s;
svg {
border-radius: 5px;
}
&:hover {
color: $darkestgrey;
opacity: 0.5;
svg {
background: #272931;
}
}
}
}
@ -187,3 +195,42 @@
color: $white;
}
}
.spinner {
height: 10px;
width: 10px;
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);
}
}

121
app/components/Contacts/SubmitChannelForm.js

@ -0,0 +1,121 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaAngleDown } from 'react-icons/lib/fa'
import styles from './SubmitChannelForm.scss'
class SubmitChannelForm extends React.Component {
render() {
const {
closeChannelForm,
closeContactsForm,
node,
contactCapacity,
updateContactCapacity,
openChannel,
toggleCurrencyProps: {
setContactsCurrencyFilters,
showCurrencyFilters,
currencyName,
currentCurrencyFilters,
onCurrencyFilterClick,
contactFormUsdAmount
}
} = this.props
const renderTitle = () => {
// if the node has an alias set we will show that with the pubkey in parens
// if not, just show the pubkey (would look ugly with rando parens)
if (node.alias && node.alias.length) {
return `${node.alias} (${node.pub_key})`
}
return node.pub_key
}
const formSubmitted = () => {
// submit the channel to LND
openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: contactCapacity })
// close the ChannelForm component
closeChannelForm()
// close the AddChannel component
closeContactsForm()
}
return (
<div className={styles.content}>
<header className={styles.header}>
<h1>Add Funds to Network</h1>
<p>
Adding a connection will help you send and receive money on the Lightning Network.
You aren&apos;t spening any money, rather moving the money you plan to use onto the network.
</p>
</header>
<section className={styles.title}>
<h2>{renderTitle()}</h2>
</section>
<section className={styles.amount}>
<div className={styles.input}>
<input
type='number'
min='0'
size=''
placeholder='0.00000000'
value={contactCapacity || ''}
onChange={event => updateContactCapacity(event.target.value)}
id='amount'
/>
<div className={styles.currency}>
<section className={styles.currentCurrency} onClick={() => setContactsCurrencyFilters(!showCurrencyFilters)}>
<span>{currencyName}</span><span><FaAngleDown /></span>
</section>
<ul className={showCurrencyFilters && styles.active}>
{
currentCurrencyFilters.map(filter =>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>)
}
</ul>
</div>
</div>
<div className={styles.usdAmount}>
{`${contactFormUsdAmount || 0} USD`}
</div>
</section>
<section className={styles.submit}>
<div
className={`${styles.button} ${contactCapacity > 0 && styles.active}`}
onClick={formSubmitted}
>
Submit
</div>
</section>
</div>
)
}
}
SubmitChannelForm.propTypes = {
closeChannelForm: PropTypes.func.isRequired,
closeContactsForm: PropTypes.func.isRequired,
node: PropTypes.object.isRequired,
contactCapacity: PropTypes.PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
updateContactCapacity: PropTypes.func.isRequired,
openChannel: PropTypes.func.isRequired,
toggleCurrencyProps: PropTypes.object.isRequired
}
export default SubmitChannelForm

150
app/components/Contacts/SubmitChannelForm.scss

@ -0,0 +1,150 @@
@import '../../variables.scss';
.content {
padding: 0 40px;
font-family: Roboto;
color: $white;
.header {
padding: 20px 100px;
h1 {
margin-bottom: 15px;
font-size: 20px;
}
p {
text-align: center;
line-height: 1.3;
font-size: 12px;
}
}
}
.header {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid $spaceborder;
h1 {
font-size: 22px;
font-weight: 100;
margin-top: 10px;
letter-spacing: 1.5px;
}
}
.title {
margin: 50px 0;
h2 {
font-size: 14px;
background: $spaceblue;
padding: 10px;
border-radius: 17.5px;
display: inline;
}
}
.input {
display: flex;
flex-direction: row;
align-items: center;
input {
font-size: 40px;
max-width: 230px;
}
}
.input input {
background: transparent;
outline: none;
border: 0;
color: $gold;
-webkit-text-fill-color: $white;
width: 100%;
font-weight: 200;
}
.input input::-webkit-input-placeholder, ::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.currency {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
.currentCurrency {
cursor: pointer;
transition: 0.25s all;
&:hover {
opacity: 0.5;
}
span {
font-size: 14px;
&:nth-child(1) {
font-weight: bold;
}
}
}
ul {
visibility: hidden;
position: absolute;
top: 30px;
&.active {
visibility: visible;
}
li {
padding: 8px 15px;
background: #191919;
cursor: pointer;
transition: 0.25s hover;
border-bottom: 1px solid #0f0f0f;
&:hover {
background: #0f0f0f;
}
}
}
}
.usdAmount {
margin-top: 20px;
opacity: 0.5;
}
.submit {
margin-top: 50px;
text-align: center;
.button {
width: 235px;
margin: 0 auto;
padding: 20px 10px;
background: #31343f;
opacity: 0.5;
cursor: pointer;
transition: 0.25s all;
&.active {
background: $gold;
opacity: 1.0;
&:hover {
background: darken($gold, 5%);
}
}
}
}

2
app/components/Form/Form.js

@ -19,7 +19,7 @@ const Form = ({ formType, formProps, closeForm }) => {
const FormComponent = FORM_TYPES[formType]
return (
<div className={`${styles.container} ${formType && styles.open}`}>
<div className={styles.container}>
<div className={styles.closeContainer}>
<span onClick={closeForm}>
<Isvg src={x} />

7
app/components/Form/Form.scss

@ -1,8 +1,11 @@
@import '../../variables.scss';
.container {
position: relative;
position: absolute;
top: 0;
z-index: 10;
height: 100vh;
width: 100%;
background: $spaceblue;
}
@ -23,4 +26,4 @@
svg {
color: $white;
}
}
}

4
app/main.dev.js

@ -164,7 +164,7 @@ const startLnd = (alias, autopilot) => {
'--bitcoin.active',
'--bitcoin.testnet',
'--bitcoin.node=neutrino',
'--neutrino.connect=btcd.jackmallers.com:18333',
'--neutrino.connect=btcd0.lightning.computer:18333',
'--neutrino.addpeer=btcd.jackmallers.com:18333',
'--neutrino.addpeer=159.65.48.139:18333',
'--neutrino.connect=127.0.0.1:18333',
@ -200,7 +200,7 @@ const startLnd = (alias, autopilot) => {
}, 1000)
}
if (line.includes('The wallet has been unlocked')) {
if (line.includes('gRPC proxy started') && !line.includes('password')) {
console.log('WALLET OPENED, STARTING LIGHTNING GRPC CONNECTION')
sendLndSyncing()
startGrpc()

130
app/reducers/channelform.js

@ -1,130 +0,0 @@
import { createSelector } from 'reselect'
// Initial State
const initialState = {
isOpen: false,
node_key: '',
local_amt: 0,
push_amt: 0,
step: 1
}
// Constants
// ------------------------------------
export const OPEN_CHANNEL_FORM = 'OPEN_CHANNEL_FORM'
export const CLOSE_CHANNEL_FORM = 'CLOSE_CHANNEL_FORM'
export const SET_NODE_KEY = 'SET_NODE_KEY'
export const SET_LOCAL_AMOUNT = 'SET_LOCAL_AMOUNT'
export const SET_PUSH_AMOUNT = 'SET_PUSH_AMOUNT'
export const CHANGE_STEP = 'CHANGE_STEP'
export const RESET_CHANNEL_FORM = 'RESET_CHANNEL_FORM'
// ------------------------------------
// Actions
// ------------------------------------
export function openChannelForm() {
return {
type: OPEN_CHANNEL_FORM
}
}
export function closeChannelForm() {
return {
type: CLOSE_CHANNEL_FORM
}
}
export function setNodeKey(node_key) {
return {
type: SET_NODE_KEY,
node_key
}
}
export function setLocalAmount(local_amt) {
return {
type: SET_LOCAL_AMOUNT,
local_amt
}
}
export function setPushAmount(push_amt) {
return {
type: SET_PUSH_AMOUNT,
push_amt
}
}
export function changeStep(step) {
return {
type: CHANGE_STEP,
step
}
}
export function resetChannelForm() {
return {
type: RESET_CHANNEL_FORM
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[OPEN_CHANNEL_FORM]: state => ({ ...state, isOpen: true }),
[CLOSE_CHANNEL_FORM]: state => ({ ...state, isOpen: false }),
[SET_NODE_KEY]: (state, { node_key }) => ({ ...state, node_key }),
[SET_LOCAL_AMOUNT]: (state, { local_amt }) => ({ ...state, local_amt }),
[SET_PUSH_AMOUNT]: (state, { push_amt }) => ({ ...state, push_amt }),
[CHANGE_STEP]: (state, { step }) => ({ ...state, step }),
[RESET_CHANNEL_FORM]: () => (initialState)
}
const channelFormSelectors = {}
const channelFormStepSelector = state => state.channelform.step
const channelFormLocalAmountSelector = state => state.channelform.local_amt
channelFormSelectors.channelFormHeader = createSelector(
channelFormStepSelector,
(step) => {
switch (step) {
case 1:
return 'Step 1: Select a peer'
case 2:
return 'Step 2: Set your local amount'
case 3:
return 'Step 3: Set your push amount'
default:
return 'Step 4: Create your channel'
}
}
)
channelFormSelectors.channelFormProgress = createSelector(
channelFormStepSelector,
step => ((step - 1) / 3) * 100
)
channelFormSelectors.stepTwoIsValid = createSelector(
channelFormLocalAmountSelector,
local_amt => local_amt > 0
)
export { channelFormSelectors }
// ------------------------------------
// Reducer
// ------------------------------------
export default function channelFormReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

5
app/reducers/channels.js

@ -3,7 +3,6 @@ import { ipcRenderer } from 'electron'
import filter from 'lodash/filter'
import { btc } from 'utils'
import { showNotification } from 'notifications'
import { closeChannelForm, resetChannelForm } from './channelform'
import { setError } from './error'
// ------------------------------------
// Constants
@ -167,8 +166,6 @@ export const openChannel = ({
// Receive IPC event for openChannel
export const channelSuccessful = () => (dispatch) => {
dispatch(fetchChannels())
dispatch(closeChannelForm())
dispatch(resetChannelForm())
}
// Receive IPC event for updated channel
@ -199,8 +196,6 @@ export const closeChannel = ({ channel_point, chan_id, force }) => (dispatch) =>
dispatch(closingChannel())
dispatch(addClosingChanId(chan_id))
console.log('force: ', force)
const [funding_txid, output_index] = channel_point.split(':')
ipcRenderer.send(
'lnd',

127
app/reducers/contactsform.js

@ -2,15 +2,29 @@ import { createSelector } from 'reselect'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty'
import { tickerSelectors } from './ticker'
import { btc } from '../utils'
// Initial State
const initialState = {
// this determines whether or not the network side bar is in search state for a peer or not
isOpen: false,
// this determines what form (manual or submit) the user currently has open
// if this is not null the ChannelForm component will be open
formType: null,
searchQuery: '',
manualSearchQuery: '',
contactCapacity: 0.1,
node: {},
showErrors: {
manualInput: false
}
},
manualFormOpen: false,
submitChannelFormOpen: false,
showCurrencyFilters: false
}
// Constants
@ -18,6 +32,19 @@ const initialState = {
export const OPEN_CONTACTS_FORM = 'OPEN_CONTACTS_FORM'
export const CLOSE_CONTACTS_FORM = 'CLOSE_CONTACTS_FORM'
export const OPEN_CHANNEL_FORM_FORM = 'OPEN_CHANNEL_FORM_FORM'
export const CLOSE_CHANNEL_FORM_FORM = 'CLOSE_CHANNEL_FORM_FORM'
export const SET_CHANNEL_FORM_TYPE = 'SET_CHANNEL_FORM_TYPE'
export const OPEN_MANUAL_FORM = 'OPEN_MANUAL_FORM'
export const CLOSE_MANUAL_FORM = 'CLOSE_MANUAL_FORM'
export const OPEN_SUBMIT_CHANNEL_FORM = 'OPEN_SUBMIT_CHANNEL_FORM'
export const CLOSE_SUBMIT_CHANNEL_FORM = 'CLOSE_SUBMIT_CHANNEL_FORM'
export const SET_NODE = 'SET_NODE'
export const UPDATE_CONTACT_FORM_SEARCH_QUERY = 'UPDATE_CONTACT_FORM_SEARCH_QUERY'
export const UPDATE_CONTACT_CAPACITY = 'UPDATE_CONTACT_CAPACITY'
@ -26,6 +53,8 @@ export const UPDATE_MANUAL_FORM_ERRORS = 'UPDATE_MANUAL_FORM_ERRORS'
export const UPDATE_MANUAL_FORM_SEARCH_QUERY = 'UPDATE_MANUAL_FORM_SEARCH_QUERY'
export const SET_CONTACTS_CURRENCY_FILTERS = 'SET_CONTACTS_CURRENCY_FILTERS'
// ------------------------------------
// Actions
// ------------------------------------
@ -41,6 +70,49 @@ export function closeContactsForm() {
}
}
export function openChannelForm() {
return {
type: OPEN_CONTACTS_FORM
}
}
export function closeChannelForm() {
return {
type: CLOSE_CONTACTS_FORM
}
}
export function setChannelFormType(formType) {
return {
type: SET_CHANNEL_FORM_TYPE,
formType
}
}
export function openManualForm() {
return {
type: OPEN_MANUAL_FORM
}
}
export function closeManualForm() {
return {
type: CLOSE_MANUAL_FORM
}
}
export function openSubmitChannelForm() {
return {
type: OPEN_SUBMIT_CHANNEL_FORM
}
}
export function closeSubmitChannelForm() {
return {
type: CLOSE_SUBMIT_CHANNEL_FORM
}
}
export function updateContactFormSearchQuery(searchQuery) {
return {
type: UPDATE_CONTACT_FORM_SEARCH_QUERY,
@ -62,6 +134,13 @@ export function updateContactCapacity(contactCapacity) {
}
}
export function setNode(node) {
return {
type: SET_NODE,
node
}
}
export function updateManualFormErrors(errorsObject) {
return {
type: UPDATE_MANUAL_FORM_ERRORS,
@ -69,6 +148,13 @@ export function updateManualFormErrors(errorsObject) {
}
}
export function setContactsCurrencyFilters(showCurrencyFilters) {
return {
type: SET_CONTACTS_CURRENCY_FILTERS,
showCurrencyFilters
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -76,15 +162,27 @@ const ACTION_HANDLERS = {
[OPEN_CONTACTS_FORM]: state => ({ ...state, isOpen: true }),
[CLOSE_CONTACTS_FORM]: state => ({ ...state, isOpen: false }),
[SET_CHANNEL_FORM_TYPE]: (state, { formType }) => ({ ...state, formType }),
[OPEN_MANUAL_FORM]: state => ({ ...state, manualFormOpen: true }),
[CLOSE_MANUAL_FORM]: state => ({ ...state, manualFormOpen: false }),
[OPEN_SUBMIT_CHANNEL_FORM]: state => ({ ...state, submitChannelFormOpen: true }),
[CLOSE_SUBMIT_CHANNEL_FORM]: state => ({ ...state, submitChannelFormOpen: false }),
[UPDATE_CONTACT_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }),
[UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }),
[UPDATE_CONTACT_CAPACITY]: (state, { contactCapacity }) => ({ ...state, contactCapacity }),
[SET_NODE]: (state, { node }) => ({ ...state, node }),
[UPDATE_MANUAL_FORM_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }),
[UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { manualSearchQuery }) => ({ ...state, manualSearchQuery })
[UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { manualSearchQuery }) => ({ ...state, manualSearchQuery }),
[SET_CONTACTS_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ({ ...state, showCurrencyFilters })
}
// ------------------------------------
@ -94,6 +192,8 @@ const contactFormSelectors = {}
const networkNodesSelector = state => state.network.nodes
const searchQuerySelector = state => state.contactsform.searchQuery
const manualSearchQuerySelector = state => state.contactsform.manualSearchQuery
const contactCapacitySelector = state => state.contactsform.contactCapacity
const currencySelector = state => state.ticker.currency
const contactable = node => (
node.addresses.length > 0
@ -115,13 +215,21 @@ contactFormSelectors.filteredNetworkNodes = createSelector(
(nodes, searchQuery) => {
// If there is no search query default to showing the first 20 nodes from the nodes array
// (performance hit to render the entire thing by default)
if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) }
// if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) }
// return an empty array if there is no search query
if (!searchQuery.length) { return [] }
// if there is an '@' in the search query we are assuming they are using the format pubkey@host
// we can ignore the '@' and the host and just grab the pubkey for our search
const query = searchQuery.includes('@') ? searchQuery.split('@')[0] : searchQuery
return filter(nodes, node => node.alias.includes(query) || node.pub_key.includes(query)).sort(contactableFirst)
// list of the nodes
const list = filter(nodes, node => node.alias.includes(query) || node.pub_key.includes(query)).sort(contactableFirst)
// if we don't limit the nodes returned then we take a huge performance hit
// rendering thousands of nodes potentially, so we just render 20 for the time being
return list.slice(0, 20)
}
)
@ -153,6 +261,17 @@ contactFormSelectors.manualFormIsValid = createSelector(
}
)
contactFormSelectors.contactFormUsdAmount = createSelector(
contactCapacitySelector,
currencySelector,
tickerSelectors.currentTicker,
(amount, currency, ticker) => {
if (!ticker || !ticker.price_usd) { return false }
return btc.convert(currency, 'usd', amount, ticker.price_usd)
}
)
export { contactFormSelectors }

2
app/reducers/index.js

@ -8,7 +8,6 @@ import balance from './balance'
import payment from './payment'
import peers from './peers'
import channels from './channels'
import channelform from './channelform'
import contactsform from './contactsform'
@ -34,7 +33,6 @@ const rootReducer = combineReducers({
payment,
peers,
channels,
channelform,
contactsform,
form,

14
app/routes/app/components/App.js

@ -5,11 +5,12 @@ import GlobalError from 'components/GlobalError'
import LoadingBolt from 'components/LoadingBolt'
import Form from 'components/Form'
import ChannelForm from 'components/Contacts/ChannelForm'
import ModalRoot from 'components/ModalRoot'
import Network from 'components/Contacts/Network'
import AddChannel from 'components/Contacts/AddChannel'
import ContactModal from 'components/Contacts/ContactModal'
import ContactsForm from 'components/Contacts/ContactsForm'
import ReceiveModal from 'components/Wallet/ReceiveModal'
import ActivityModal from 'components/Activity/ActivityModal'
@ -60,6 +61,7 @@ class App extends Component {
networkTabProps,
receiveModalProps,
activityModalProps,
channelFormProps,
children
} = this.props
@ -79,9 +81,9 @@ class App extends Component {
/>
<ContactModal {...contactModalProps} />
<ContactsForm {...contactsFormProps} />
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
<ChannelForm {...channelFormProps} />
<ReceiveModal {...receiveModalProps} />
<ActivityModal {...activityModalProps} />
@ -90,7 +92,12 @@ class App extends Component {
{children}
</div>
<Network {...networkTabProps} />
{
contactsFormProps.contactsform.isOpen ?
<AddChannel {...contactsFormProps} />
:
<Network {...networkTabProps} />
}
</div>
)
}
@ -109,6 +116,7 @@ App.propTypes = {
networkTabProps: PropTypes.object,
activityModalProps: PropTypes.object,
receiveModalProps: PropTypes.object,
channelFormProps: PropTypes.object,
newAddress: PropTypes.func.isRequired,
fetchInfo: PropTypes.func.isRequired,

2
app/routes/app/components/App.scss

@ -2,7 +2,7 @@
.content {
position: relative;
width: 80%;
width: 70%;
display: inline-block;
vertical-align: top;
overflow-y: auto;

89
app/routes/app/containers/AppContainer.js

@ -1,6 +1,8 @@
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { btc } from 'utils'
import { fetchTicker, setCurrency, tickerSelectors } from 'reducers/ticker'
import { newAddress, closeWalletModal } from 'reducers/address'
@ -39,11 +41,24 @@ import {
import {
openContactsForm,
closeContactsForm,
setChannelFormType,
openManualForm,
closeManualForm,
openSubmitChannelForm,
closeSubmitChannelForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
updateContactCapacity,
setNode,
contactFormSelectors,
updateManualFormErrors
updateManualFormErrors,
setContactsCurrencyFilters
} from 'reducers/contactsform'
import { fetchBalance } from 'reducers/balance'
@ -100,11 +115,18 @@ const mapDispatchToProps = {
openContactsForm,
closeContactsForm,
openSubmitChannelForm,
closeSubmitChannelForm,
openManualForm,
closeManualForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
updateContactCapacity,
setNode,
contactFormSelectors,
updateManualFormErrors,
setContactsCurrencyFilters,
setChannelFormType,
fetchDescribeNetwork,
@ -155,6 +177,7 @@ const mapStateToProps = state => ({
filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state),
showManualForm: contactFormSelectors.showManualForm(state),
manualFormIsValid: contactFormSelectors.manualFormIsValid(state),
contactFormUsdAmount: contactFormSelectors.contactFormUsdAmount(state),
currentChannels: currentChannels(state),
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state),
@ -292,11 +315,14 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
const contactsFormProps = {
closeContactsForm: dispatchProps.closeContactsForm,
openSubmitChannelForm: () => dispatchProps.setChannelFormType('SUBMIT_CHANNEL_FORM'),
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery,
updateManualFormSearchQuery: dispatchProps.updateManualFormSearchQuery,
updateContactCapacity: dispatchProps.updateContactCapacity,
setNode: dispatchProps.setNode,
openChannel: dispatchProps.openChannel,
updateManualFormErrors: dispatchProps.updateManualFormErrors,
openManualForm: () => dispatchProps.setChannelFormType('MANUAL_FORM'),
contactsform: stateProps.contactsform,
filteredNetworkNodes: stateProps.filteredNetworkNodes,
@ -348,6 +374,61 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
closeReceiveModal: dispatchProps.closeWalletModal
}
const submitChannelFormProps = {
submitChannelFormOpen: stateProps.contactsform.submitChannelFormOpen,
node: stateProps.contactsform.node,
contactCapacity: stateProps.contactsform.contactCapacity,
updateContactCapacity: dispatchProps.updateContactCapacity,
closeChannelForm: () => dispatchProps.setChannelFormType(null),
closeContactsForm: dispatchProps.closeContactsForm,
openChannel: dispatchProps.openChannel,
toggleCurrencyProps: {
currentCurrencyFilters: stateProps.currentCurrencyFilters,
currencyName: stateProps.currencyName,
showCurrencyFilters: stateProps.contactsform.showCurrencyFilters,
contactFormUsdAmount: stateProps.contactFormUsdAmount,
setContactsCurrencyFilters: dispatchProps.setContactsCurrencyFilters,
setCurrencyFilters: dispatchProps.setCurrencyFilters,
onCurrencyFilterClick: (currency) => {
dispatchProps.updateContactCapacity(btc.convert(stateProps.ticker.currency, currency, stateProps.contactsform.contactCapacity))
dispatchProps.setCurrency(currency)
dispatchProps.setContactsCurrencyFilters(false)
}
}
}
const connectManuallyProps = {
closeManualForm: dispatchProps.closeManualForm,
updateManualFormSearchQuery: dispatchProps.updateManualFormSearchQuery,
updateManualFormErrors: dispatchProps.updateManualFormErrors,
setNode: dispatchProps.setNode,
openSubmitChannelForm: () => dispatchProps.setChannelFormType('SUBMIT_CHANNEL_FORM'),
manualFormOpen: stateProps.contactsform.manualFormOpen,
manualSearchQuery: stateProps.contactsform.manualSearchQuery,
manualFormIsValid: stateProps.manualFormIsValid,
showErrors: stateProps.contactsform.showErrors
}
const calcChannelFormProps = (formType) => {
if (formType === 'MANUAL_FORM') { return connectManuallyProps }
if (formType === 'SUBMIT_CHANNEL_FORM') { return submitChannelFormProps }
return {}
}
const channelFormProps = {
formType: stateProps.contactsform.formType,
formProps: calcChannelFormProps(stateProps.contactsform.formType),
closeForm: () => dispatchProps.setChannelFormType(null)
}
return {
...stateProps,
...dispatchProps,
@ -363,6 +444,12 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
receiveModalProps,
// props for the activity modals
activityModalProps,
// props for the form to open a channel
submitChannelFormProps,
// props for the form to connect manually to a peer
connectManuallyProps,
// props for the channel form wrapper
channelFormProps,
// Props to pass to the pay form
formProps: formProps(stateProps.form.formType),
// action to close form

57
test/reducers/__snapshots__/channels.spec.js.snap

@ -229,63 +229,6 @@ Object {
}
`;
exports[`reducers channelsReducer should correctly setChannelForm 1`] = `
Object {
"channel": null,
"channelForm": Object {
"isOpen": true,
"local_amt": "",
"node_key": "",
"push_amt": "",
},
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"closingChannelIds": Array [],
"contactModal": Object {
"channel": null,
"isOpen": false,
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
"pending_force_closing_channels": Array [],
"pending_open_channels": Array [],
"total_limbo_balance": "",
},
"searchQuery": "",
"viewType": 0,
}
`;
exports[`reducers channelsReducer should handle initial state 1`] = `
Object {
"channel": null,

4
test/reducers/channels.spec.js

@ -46,10 +46,6 @@ describe('reducers', () => {
expect(channelsReducer(undefined, { type: SET_CHANNEL, channel: 'channel' })).toMatchSnapshot()
})
it('should correctly setChannelForm', () => {
expect(channelsReducer(undefined, { type: SET_CHANNEL_FORM, form: { isOpen: true } })).toMatchSnapshot()
})
it('should correctly getChannels', () => {
expect(channelsReducer(undefined, { type: GET_CHANNELS })).toMatchSnapshot()
})

Loading…
Cancel
Save