committed by
GitHub
95 changed files with 1872 additions and 3199 deletions
@ -1,90 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import ReactModal from 'react-modal' |
|
||||
|
|
||||
import { FaClose } from 'react-icons/lib/fa' |
|
||||
|
|
||||
import StepOne from './StepOne' |
|
||||
import StepTwo from './StepTwo' |
|
||||
import StepThree from './StepThree' |
|
||||
import StepFour from './StepFour' |
|
||||
import Footer from './Footer' |
|
||||
|
|
||||
import styles from './ChannelForm.scss' |
|
||||
|
|
||||
const ChannelForm = ({ |
|
||||
channelform, |
|
||||
openChannel, |
|
||||
closeChannelForm, |
|
||||
changeStep, |
|
||||
setNodeKey, |
|
||||
setLocalAmount, |
|
||||
setPushAmount, |
|
||||
channelFormHeader, |
|
||||
channelFormProgress, |
|
||||
stepTwoIsValid, |
|
||||
peers |
|
||||
}) => { |
|
||||
const renderStep = () => { |
|
||||
const { step } = channelform |
|
||||
|
|
||||
switch (step) { |
|
||||
case 1: |
|
||||
return <StepOne peers={peers} changeStep={changeStep} setNodeKey={setNodeKey} /> |
|
||||
case 2: |
|
||||
return <StepTwo local_amt={channelform.local_amt} setLocalAmount={setLocalAmount} /> |
|
||||
case 3: |
|
||||
return <StepThree push_amt={channelform.push_amt} setPushAmount={setPushAmount} /> |
|
||||
default: |
|
||||
return <StepFour node_key={channelform.node_key} local_amt={channelform.local_amt} push_amt={channelform.push_amt} /> |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<ReactModal |
|
||||
isOpen={channelform.isOpen} |
|
||||
ariaHideApp |
|
||||
shouldCloseOnOverlayClick |
|
||||
contentLabel='No Overlay Click Modal' |
|
||||
onRequestClose={closeChannelForm} |
|
||||
parentSelector={() => document.body} |
|
||||
className={styles.modal} |
|
||||
> |
|
||||
<div onClick={closeChannelForm} className={styles.modalClose}> |
|
||||
<FaClose /> |
|
||||
</div> |
|
||||
|
|
||||
<header className={styles.header}> |
|
||||
<h3>{channelFormHeader}</h3> |
|
||||
<div className={styles.progress} style={{ width: `${channelFormProgress}%` }} /> |
|
||||
</header> |
|
||||
|
|
||||
<div className={styles.content}> |
|
||||
{renderStep()} |
|
||||
</div> |
|
||||
|
|
||||
<Footer |
|
||||
step={channelform.step} |
|
||||
changeStep={changeStep} |
|
||||
stepTwoIsValid={stepTwoIsValid} |
|
||||
submit={() => openChannel({ pubkey: channelform.node_key, local_amt: channelform.local_amt, push_amt: channelform.push_amt })} |
|
||||
/> |
|
||||
</ReactModal> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
ChannelForm.propTypes = { |
|
||||
channelform: PropTypes.object.isRequired, |
|
||||
openChannel: PropTypes.func.isRequired, |
|
||||
closeChannelForm: PropTypes.func.isRequired, |
|
||||
changeStep: PropTypes.func.isRequired, |
|
||||
setNodeKey: PropTypes.func.isRequired, |
|
||||
setLocalAmount: PropTypes.func.isRequired, |
|
||||
setPushAmount: PropTypes.func.isRequired, |
|
||||
channelFormHeader: PropTypes.string.isRequired, |
|
||||
channelFormProgress: PropTypes.number.isRequired, |
|
||||
stepTwoIsValid: PropTypes.bool.isRequired, |
|
||||
peers: PropTypes.array.isRequired |
|
||||
} |
|
||||
|
|
||||
export default ChannelForm |
|
@ -1,57 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.modal { |
|
||||
width: 40%; |
|
||||
margin: 50px auto; |
|
||||
position: absolute; |
|
||||
top: auto; |
|
||||
left: 20%; |
|
||||
right: 0; |
|
||||
bottom: auto; |
|
||||
background: $white; |
|
||||
outline: none; |
|
||||
z-index: -2; |
|
||||
border: 1px solid $darkgrey; |
|
||||
} |
|
||||
|
|
||||
.modalClose { |
|
||||
position: absolute; |
|
||||
top: -13px; |
|
||||
right: -13px; |
|
||||
display: block; |
|
||||
font-size: 16px; |
|
||||
line-height: 27px; |
|
||||
width: 32px; |
|
||||
height: 32px; |
|
||||
background: $white; |
|
||||
border-radius: 50%; |
|
||||
color: $darkestgrey; |
|
||||
cursor: pointer; |
|
||||
text-align: center; |
|
||||
z-index: 2; |
|
||||
transition: all 0.25s; |
|
||||
} |
|
||||
|
|
||||
.modalClose:hover { |
|
||||
background: $darkgrey; |
|
||||
} |
|
||||
|
|
||||
.header { |
|
||||
padding: 20px; |
|
||||
background: $lightgrey; |
|
||||
text-align: center; |
|
||||
font-family: 'Jigsaw Light'; |
|
||||
text-transform: uppercase; |
|
||||
position: relative; |
|
||||
z-index: -2; |
|
||||
} |
|
||||
|
|
||||
.progress { |
|
||||
transition: all 0.2s ease; |
|
||||
background: $main; |
|
||||
position: absolute; |
|
||||
height: 100%; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
z-index: -1; |
|
||||
} |
|
@ -1,44 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import styles from './Footer.scss' |
|
||||
|
|
||||
const Footer = ({ step, changeStep, stepTwoIsValid, submit }) => { |
|
||||
if (step === 1) { return null } |
|
||||
|
|
||||
// See if the next button on step 2 should be active
|
|
||||
const nextIsInactive = step === 2 && !stepTwoIsValid |
|
||||
|
|
||||
// Function that's called when the user clicks "next" in the form
|
|
||||
const nextFunc = () => { |
|
||||
if (nextIsInactive) { return } |
|
||||
|
|
||||
changeStep(step + 1) |
|
||||
} |
|
||||
|
|
||||
const rightButtonText = step === 4 ? 'Submit' : 'Next' |
|
||||
const rightButtonOnClick = step === 4 ? () => submit() : nextFunc |
|
||||
|
|
||||
return ( |
|
||||
<div className={styles.footer}> |
|
||||
<div className='buttonContainer'> |
|
||||
<div className='buttonPrimary' onClick={() => changeStep(step - 1)}> |
|
||||
Back |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className='buttonContainer' onClick={rightButtonOnClick}> |
|
||||
<div className={`buttonPrimary ${nextIsInactive && 'inactive'}`}> |
|
||||
{rightButtonText} |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Footer.propTypes = { |
|
||||
step: PropTypes.number.isRequired, |
|
||||
changeStep: PropTypes.func.isRequired, |
|
||||
stepTwoIsValid: PropTypes.bool.isRequired, |
|
||||
submit: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default Footer |
|
@ -1,17 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.footer { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
padding-bottom: 30px; |
|
||||
|
|
||||
div { |
|
||||
margin: 0 20px; |
|
||||
|
|
||||
div { |
|
||||
padding: 18px 60px 15px 60px; |
|
||||
color: $black; |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,37 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import styles from './StepFour.scss' |
|
||||
|
|
||||
const StepFour = ({ node_key, local_amt, push_amt }) => ( |
|
||||
<div className={styles.container}> |
|
||||
<div className={styles.nodekey}> |
|
||||
<h4>Peer</h4> |
|
||||
<h2>{node_key}</h2> |
|
||||
</div> |
|
||||
|
|
||||
<div className={styles.amounts}> |
|
||||
<div className={styles.localamt}> |
|
||||
<h4>Local Amount</h4> |
|
||||
<h3>{local_amt}</h3> |
|
||||
</div> |
|
||||
<div className={styles.pushamt}> |
|
||||
<h4>Push Amount</h4> |
|
||||
<h3>{push_amt}</h3> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
) |
|
||||
|
|
||||
StepFour.propTypes = { |
|
||||
node_key: PropTypes.string.isRequired, |
|
||||
local_amt: PropTypes.oneOfType([ |
|
||||
PropTypes.number, |
|
||||
PropTypes.string |
|
||||
]), |
|
||||
push_amt: PropTypes.oneOfType([ |
|
||||
PropTypes.number, |
|
||||
PropTypes.string |
|
||||
]) |
|
||||
} |
|
||||
|
|
||||
export default StepFour |
|
@ -1,36 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.container { |
|
||||
padding: 50px; |
|
||||
|
|
||||
h4 { |
|
||||
text-transform: uppercase; |
|
||||
font-size: 14px; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
h3 { |
|
||||
text-align: center; |
|
||||
color: $main; |
|
||||
font-size: 50px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.nodekey { |
|
||||
margin-bottom: 50px; |
|
||||
padding: 20px; |
|
||||
border-bottom: 1px solid $main; |
|
||||
|
|
||||
h2 { |
|
||||
font-size: 12px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.amounts { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-around; |
|
||||
margin-bottom: 50px; |
|
||||
padding: 20px; |
|
||||
} |
|
@ -1,75 +0,0 @@ |
|||||
import React, { Component } from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import { MdSearch } from 'react-icons/lib/md' |
|
||||
import styles from './StepOne.scss' |
|
||||
|
|
||||
class StepOne extends Component { |
|
||||
constructor(props) { |
|
||||
super(props) |
|
||||
this.state = { |
|
||||
peers: props.peers, |
|
||||
searchQuery: '' |
|
||||
} |
|
||||
|
|
||||
this.onSearchQuery = this.onSearchQuery.bind(this) |
|
||||
this.peerClicked = this.peerClicked.bind(this) |
|
||||
} |
|
||||
|
|
||||
onSearchQuery(searchQuery) { |
|
||||
const peers = this.props.peers.filter(peer => peer.pub_key.includes(searchQuery)) |
|
||||
|
|
||||
this.setState({ peers, searchQuery }) |
|
||||
} |
|
||||
|
|
||||
peerClicked(peer) { |
|
||||
const { setNodeKey, changeStep } = this.props |
|
||||
|
|
||||
setNodeKey(peer.pub_key) |
|
||||
changeStep(2) |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { peers, searchQuery } = this.state |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
<div className={styles.search}> |
|
||||
<label className={`${styles.label} ${styles.input}`} htmlFor='peersSearch'> |
|
||||
<MdSearch /> |
|
||||
</label> |
|
||||
<input |
|
||||
value={searchQuery} |
|
||||
onChange={event => this.onSearchQuery(event.target.value)} |
|
||||
className={`${styles.text} ${styles.input}`} |
|
||||
placeholder='Search your peers by their public key' |
|
||||
type='text' |
|
||||
id='peersSearch' |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<ul className={styles.peers}> |
|
||||
{peers.length > 0 && |
|
||||
peers.map(peer => ( |
|
||||
<li |
|
||||
key={peer.peer_id} |
|
||||
className={styles.peer} |
|
||||
onClick={() => this.peerClicked(peer)} |
|
||||
> |
|
||||
<h4>{peer.address}</h4> |
|
||||
<h1>{peer.pub_key}</h1> |
|
||||
</li> |
|
||||
) |
|
||||
)} |
|
||||
</ul> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
StepOne.propTypes = { |
|
||||
peers: PropTypes.array.isRequired, |
|
||||
setNodeKey: PropTypes.func.isRequired, |
|
||||
changeStep: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default StepOne |
|
@ -1,74 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.peers { |
|
||||
h2 { |
|
||||
text-transform: uppercase; |
|
||||
font-weight: 200; |
|
||||
padding: 10px 0; |
|
||||
border-bottom: 1px solid $grey; |
|
||||
color: $darkestgrey; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.search { |
|
||||
height: 50px; |
|
||||
padding: 2px; |
|
||||
border-bottom: 1px solid $darkgrey; |
|
||||
|
|
||||
.input { |
|
||||
display: inline-block; |
|
||||
vertical-align: top; |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
.label { |
|
||||
width: 5%; |
|
||||
line-height: 50px; |
|
||||
font-size: 16px; |
|
||||
text-align: center; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.text { |
|
||||
width: 95%; |
|
||||
outline: 0; |
|
||||
padding: 0; |
|
||||
border: 0; |
|
||||
border-radius: 0; |
|
||||
height: 50px; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.peer { |
|
||||
position: relative; |
|
||||
background: $white; |
|
||||
padding: 10px; |
|
||||
border-top: 1px solid $grey; |
|
||||
cursor: pointer; |
|
||||
transition: all 0.25s; |
|
||||
|
|
||||
&:hover { |
|
||||
opacity: 0.5; |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border: none; |
|
||||
} |
|
||||
|
|
||||
h4, h1 { |
|
||||
margin: 10px 0; |
|
||||
} |
|
||||
|
|
||||
h4 { |
|
||||
font-size: 12px; |
|
||||
font-weight: bold; |
|
||||
color: $black; |
|
||||
} |
|
||||
|
|
||||
h1 { |
|
||||
font-size: 14px; |
|
||||
font-weight: 200; |
|
||||
color: $main; |
|
||||
} |
|
||||
} |
|
@ -1,50 +0,0 @@ |
|||||
import React, { Component } from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import CurrencyIcon from 'components/CurrencyIcon' |
|
||||
import styles from './StepThree.scss' |
|
||||
|
|
||||
class StepThree extends Component { |
|
||||
render() { |
|
||||
const { push_amt, setPushAmount } = this.props |
|
||||
|
|
||||
return ( |
|
||||
<div className={styles.container}> |
|
||||
<div className={styles.explainer}> |
|
||||
<h2>Push Amount</h2> |
|
||||
<p> |
|
||||
The push amount is the amount of bitcoin (if any at all) you'd like |
|
||||
to "push" to the other side of the channel when it opens. |
|
||||
This amount will be set on the remote side of the channel as part of the initial commitment state. |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<form> |
|
||||
<label htmlFor='amount'> |
|
||||
<CurrencyIcon currency={'btc'} crypto={'btc'} /> |
|
||||
</label> |
|
||||
<input |
|
||||
type='number' |
|
||||
min='0' |
|
||||
max='0.16' |
|
||||
ref={(input) => { this.input = input }} |
|
||||
size='' |
|
||||
value={push_amt} |
|
||||
onChange={event => setPushAmount(event.target.value)} |
|
||||
id='amount' |
|
||||
style={{ width: isNaN((push_amt.length + 1) * 55) ? 140 : (push_amt.length + 1) * 55 }} |
|
||||
/> |
|
||||
</form> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
StepThree.propTypes = { |
|
||||
push_amt: PropTypes.oneOfType([ |
|
||||
PropTypes.number, |
|
||||
PropTypes.string |
|
||||
]).isRequired, |
|
||||
setPushAmount: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default StepThree |
|
@ -1,58 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.container { |
|
||||
margin-bottom: 50px; |
|
||||
padding: 20px; |
|
||||
|
|
||||
.explainer { |
|
||||
margin: 0px 0 50px 0; |
|
||||
padding-bottom: 20px; |
|
||||
border-bottom: 1px solid $lightgrey; |
|
||||
|
|
||||
h2 { |
|
||||
margin: 0 0 20px 0; |
|
||||
font-size: 28px; |
|
||||
} |
|
||||
|
|
||||
p { |
|
||||
line-height: 1.5; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
form { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
label { |
|
||||
height: 200px; |
|
||||
color: $main; |
|
||||
|
|
||||
svg { |
|
||||
width: 65px; |
|
||||
height: 65px; |
|
||||
} |
|
||||
|
|
||||
svg[data-icon='ltc'] { |
|
||||
margin-right: 10px; |
|
||||
|
|
||||
g { |
|
||||
transform: scale(1.75) translate(-5px, -5px); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
input[type=number] { |
|
||||
color: $main; |
|
||||
width: 30px; |
|
||||
height: 200px; |
|
||||
font-size: 100px; |
|
||||
font-weight: 200; |
|
||||
border: none; |
|
||||
outline: 0; |
|
||||
-webkit-appearance: none; |
|
||||
} |
|
||||
} |
|
@ -1,49 +0,0 @@ |
|||||
import React, { Component } from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import CurrencyIcon from 'components/CurrencyIcon' |
|
||||
import styles from './StepTwo.scss' |
|
||||
|
|
||||
class StepTwo extends Component { |
|
||||
render() { |
|
||||
const { local_amt, setLocalAmount } = this.props |
|
||||
|
|
||||
return ( |
|
||||
<div className={styles.container}> |
|
||||
<div className={styles.explainer}> |
|
||||
<h2>Local Amount</h2> |
|
||||
<p> |
|
||||
Local amount is the amount of bitcoin that you would like to commit to the channel. |
|
||||
This is the amount that will be sent in an on-chain transaction to open your Lightning channel. |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<form> |
|
||||
<label htmlFor='amount'> |
|
||||
<CurrencyIcon currency={'btc'} crypto={'btc'} /> |
|
||||
</label> |
|
||||
<input |
|
||||
type='number' |
|
||||
min='0' |
|
||||
max='0.16' |
|
||||
ref={(input) => { this.input = input }} |
|
||||
size='' |
|
||||
value={local_amt} |
|
||||
onChange={event => setLocalAmount(event.target.value)} |
|
||||
id='amount' |
|
||||
style={{ width: isNaN((local_amt.length + 1) * 55) ? 140 : (local_amt.length + 1) * 55 }} |
|
||||
/> |
|
||||
</form> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
StepTwo.propTypes = { |
|
||||
local_amt: PropTypes.oneOfType([ |
|
||||
PropTypes.string, |
|
||||
PropTypes.number |
|
||||
]).isRequired, |
|
||||
setLocalAmount: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default StepTwo |
|
@ -1,58 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.container { |
|
||||
margin-bottom: 50px; |
|
||||
padding: 20px; |
|
||||
|
|
||||
.explainer { |
|
||||
margin: 0px 0 50px 0; |
|
||||
padding-bottom: 20px; |
|
||||
border-bottom: 1px solid $lightgrey; |
|
||||
|
|
||||
h2 { |
|
||||
margin: 0 0 20px 0; |
|
||||
font-size: 28px; |
|
||||
} |
|
||||
|
|
||||
p { |
|
||||
line-height: 1.5; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
form { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
label { |
|
||||
height: 200px; |
|
||||
color: $main; |
|
||||
|
|
||||
svg { |
|
||||
width: 65px; |
|
||||
height: 65px; |
|
||||
} |
|
||||
|
|
||||
svg[data-icon='ltc'] { |
|
||||
margin-right: 10px; |
|
||||
|
|
||||
g { |
|
||||
transform: scale(1.75) translate(-5px, -5px); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
input[type=number] { |
|
||||
color: $main; |
|
||||
width: 30px; |
|
||||
height: 200px; |
|
||||
font-size: 100px; |
|
||||
font-weight: 200; |
|
||||
border: none; |
|
||||
outline: 0; |
|
||||
-webkit-appearance: none; |
|
||||
} |
|
||||
} |
|
@ -1,3 +0,0 @@ |
|||||
import ChannelForm from './ChannelForm' |
|
||||
|
|
||||
export default ChannelForm |
|
@ -1,93 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import { FaCircle } from 'react-icons/lib/fa' |
|
||||
import { btc } from 'utils' |
|
||||
import styles from './Channel.scss' |
|
||||
|
|
||||
const Channel = ({ ticker, channel, closeChannel, currentTicker }) => ( |
|
||||
<li className={styles.channel}> |
|
||||
<header className={styles.header}> |
|
||||
<div> |
|
||||
<span className={styles.status}>Open</span> |
|
||||
{ |
|
||||
channel.active ? |
|
||||
<span className={styles.active}> |
|
||||
<FaCircle /> |
|
||||
<i>Active</i> |
|
||||
</span> |
|
||||
: |
|
||||
<span className={styles.notactive}> |
|
||||
<FaCircle /> |
|
||||
<i>Not Active</i> |
|
||||
</span> |
|
||||
} |
|
||||
</div> |
|
||||
<div> |
|
||||
<p |
|
||||
className={styles.close} |
|
||||
onClick={() => closeChannel({ channel_point: channel.channel_point })} |
|
||||
> |
|
||||
Close channel |
|
||||
</p> |
|
||||
</div> |
|
||||
</header> |
|
||||
<div className={styles.content}> |
|
||||
<div className={styles.left}> |
|
||||
<section className={styles.remotePubkey}> |
|
||||
<span>Remote Pubkey</span> |
|
||||
<h4>{channel.remote_pubkey}</h4> |
|
||||
</section> |
|
||||
<section className={styles.channelPoint}> |
|
||||
<span>Channel Point</span> |
|
||||
<h4>{channel.channel_point}</h4> |
|
||||
</section> |
|
||||
</div> |
|
||||
<div className={styles.right}> |
|
||||
<section className={styles.capacity}> |
|
||||
<span>Capacity</span> |
|
||||
<h2> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.capacity) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd) |
|
||||
} |
|
||||
</h2> |
|
||||
</section> |
|
||||
<div className={styles.balances}> |
|
||||
<section> |
|
||||
<span>Local</span> |
|
||||
<h4> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.local_balance) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd) |
|
||||
} |
|
||||
</h4> |
|
||||
</section> |
|
||||
<section> |
|
||||
<span>Remote</span> |
|
||||
<h4> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.remote_balance) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd) |
|
||||
} |
|
||||
</h4> |
|
||||
</section> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</li> |
|
||||
) |
|
||||
|
|
||||
Channel.propTypes = { |
|
||||
ticker: PropTypes.object.isRequired, |
|
||||
channel: PropTypes.object.isRequired, |
|
||||
closeChannel: PropTypes.func.isRequired, |
|
||||
currentTicker: PropTypes.object.isRequired |
|
||||
} |
|
||||
|
|
||||
export default Channel |
|
@ -1,125 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.channel { |
|
||||
position: relative; |
|
||||
background: $white; |
|
||||
margin: 5px 0; |
|
||||
padding: 10px; |
|
||||
border-top: 1px solid $white; |
|
||||
cursor: pointer; |
|
||||
transition: all 0.25s; |
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); |
|
||||
|
|
||||
&:hover { |
|
||||
opacity: 0.75; |
|
||||
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border: none; |
|
||||
} |
|
||||
|
|
||||
.header { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
|
|
||||
.status, .active, .notactive { |
|
||||
padding: 10px; |
|
||||
text-transform: uppercase; |
|
||||
font-weight: bold; |
|
||||
font-size: 10px; |
|
||||
} |
|
||||
|
|
||||
.status { |
|
||||
color: $main; |
|
||||
} |
|
||||
|
|
||||
.active i, .notactive i { |
|
||||
margin-left: 5px; |
|
||||
} |
|
||||
|
|
||||
.active { |
|
||||
color: $green; |
|
||||
} |
|
||||
|
|
||||
.notactive { |
|
||||
color: $red; |
|
||||
} |
|
||||
|
|
||||
.close { |
|
||||
padding: 10px; |
|
||||
font-size: 10px; |
|
||||
text-transform: uppercase; |
|
||||
color: $red; |
|
||||
cursor: pointer; |
|
||||
|
|
||||
&:hover { |
|
||||
color: lighten($red, 10%); |
|
||||
text-decoration: underline; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.content { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
.left, .right { |
|
||||
padding: 0 10px; |
|
||||
margin-bottom: 5; |
|
||||
|
|
||||
section { |
|
||||
margin-bottom: 20px; |
|
||||
|
|
||||
span { |
|
||||
text-transform: uppercase; |
|
||||
letter-spacing: 1.6px; |
|
||||
color: $black; |
|
||||
font-size: 10px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
h2 { |
|
||||
font-size: 30px; |
|
||||
padding: 5px 0; |
|
||||
color: $main; |
|
||||
} |
|
||||
|
|
||||
h4 { |
|
||||
margin-top: 5px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.left { |
|
||||
flex: 7; |
|
||||
} |
|
||||
|
|
||||
.right { |
|
||||
flex: 3; |
|
||||
|
|
||||
.capacity { |
|
||||
text-align: center; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.balances { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
|
|
||||
section { |
|
||||
flex: 5; |
|
||||
text-align: center; |
|
||||
|
|
||||
h4 { |
|
||||
color: $main; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,127 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import ReactModal from 'react-modal' |
|
||||
import { FaUser } from 'react-icons/lib/fa' |
|
||||
import CurrencyIcon from 'components/CurrencyIcon' |
|
||||
import { usd, btc } from 'utils' |
|
||||
import styles from './ChannelForm.scss' |
|
||||
|
|
||||
const ChannelForm = ({ form, setForm, ticker, peers, openChannel, currentTicker }) => { |
|
||||
const submitClicked = () => { |
|
||||
const { node_key, local_amt, push_amt } = form |
|
||||
|
|
||||
const localamt = ticker.currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(local_amt, currentTicker.price_usd)) : btc.btcToSatoshis(local_amt) |
|
||||
const pushamt = ticker.currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(push_amt, currentTicker.price_usd)) : btc.btcToSatoshis(push_amt) |
|
||||
|
|
||||
openChannel({ pubkey: node_key, localamt, pushamt }) |
|
||||
// setForm({ isOpen: false })
|
|
||||
} |
|
||||
|
|
||||
const customStyles = { |
|
||||
overlay: { |
|
||||
cursor: 'pointer', |
|
||||
overflowY: 'auto' |
|
||||
}, |
|
||||
content: { |
|
||||
top: 'auto', |
|
||||
left: '20%', |
|
||||
right: '0', |
|
||||
bottom: 'auto', |
|
||||
width: '40%', |
|
||||
margin: '50px auto', |
|
||||
padding: '40px' |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
<ReactModal |
|
||||
isOpen={form.isOpen} |
|
||||
contentLabel='No Overlay Click Modal' |
|
||||
ariaHideApp |
|
||||
shouldCloseOnOverlayClick |
|
||||
onRequestClose={() => setForm({ isOpen: false })} |
|
||||
parentSelector={() => document.body} |
|
||||
style={customStyles} |
|
||||
> |
|
||||
<div className={styles.form}> |
|
||||
<h1 className={styles.title}>Open a new channel</h1> |
|
||||
|
|
||||
<section className={styles.pubkey}> |
|
||||
<label htmlFor='nodekey'><FaUser /></label> |
|
||||
<input |
|
||||
type='text' |
|
||||
size='' |
|
||||
placeholder='Peer public key' |
|
||||
value={form.node_key} |
|
||||
onChange={event => setForm({ node_key: event.target.value })} |
|
||||
id='nodekey' |
|
||||
/> |
|
||||
</section> |
|
||||
<section className={styles.local}> |
|
||||
<label htmlFor='localamount'> |
|
||||
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} /> |
|
||||
</label> |
|
||||
<input |
|
||||
type='text' |
|
||||
size='' |
|
||||
placeholder='Local amount' |
|
||||
value={form.local_amt} |
|
||||
onChange={event => setForm({ local_amt: event.target.value })} |
|
||||
id='localamount' |
|
||||
/> |
|
||||
</section> |
|
||||
<section className={styles.push}> |
|
||||
<label htmlFor='pushamount'> |
|
||||
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} /> |
|
||||
</label> |
|
||||
<input |
|
||||
type='text' |
|
||||
size='' |
|
||||
placeholder='Push amount' |
|
||||
value={form.push_amt} |
|
||||
onChange={event => setForm({ push_amt: event.target.value })} |
|
||||
id='pushamount' |
|
||||
/> |
|
||||
</section> |
|
||||
|
|
||||
<ul className={styles.peers}> |
|
||||
<h2>Connected Peers</h2> |
|
||||
{ |
|
||||
peers.length ? |
|
||||
peers.map(peer => |
|
||||
( |
|
||||
<li |
|
||||
key={peer.peer_id} |
|
||||
className={styles.peer} |
|
||||
onClick={() => setForm({ node_key: peer.pub_key })} |
|
||||
> |
|
||||
<h4>{peer.address}</h4> |
|
||||
<h1>{peer.pub_key}</h1> |
|
||||
</li> |
|
||||
) |
|
||||
) |
|
||||
: |
|
||||
null |
|
||||
} |
|
||||
</ul> |
|
||||
|
|
||||
<div className={styles.buttonGroup}> |
|
||||
<div className={styles.button} onClick={submitClicked}>Submit</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</ReactModal> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
ChannelForm.propTypes = { |
|
||||
form: PropTypes.object.isRequired, |
|
||||
setForm: PropTypes.func.isRequired, |
|
||||
ticker: PropTypes.object.isRequired, |
|
||||
peers: PropTypes.array.isRequired, |
|
||||
openChannel: PropTypes.func.isRequired, |
|
||||
currentTicker: PropTypes.object.isRequired |
|
||||
} |
|
||||
|
|
||||
export default ChannelForm |
|
@ -1,123 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.title { |
|
||||
text-align: center; |
|
||||
font-size: 24px; |
|
||||
color: $black; |
|
||||
margin-bottom: 50px; |
|
||||
} |
|
||||
|
|
||||
.pubkey, .local, .push { |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
font-size: 18px; |
|
||||
height: auto; |
|
||||
min-height: 55px; |
|
||||
margin-bottom: 20px; |
|
||||
border: 1px solid $traditionalgrey; |
|
||||
border-radius: 6px; |
|
||||
position: relative; |
|
||||
padding: 0 20px; |
|
||||
|
|
||||
label, input[type=text] { |
|
||||
font-size: inherit; |
|
||||
} |
|
||||
|
|
||||
label { |
|
||||
padding-top: 19px; |
|
||||
padding-bottom: 12px; |
|
||||
color: $traditionalgrey; |
|
||||
|
|
||||
svg[data-icon='ltc'] { |
|
||||
width: 18px; |
|
||||
height: 16px; |
|
||||
|
|
||||
g { |
|
||||
transform: scale(1.75) translate(-5px, -5px); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
input[type=text] { |
|
||||
width: 100%; |
|
||||
border: none; |
|
||||
outline: 0; |
|
||||
-webkit-appearance: none; |
|
||||
height: 55px; |
|
||||
padding: 0 10px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.peers { |
|
||||
margin-bottom: 50px; |
|
||||
|
|
||||
h2 { |
|
||||
text-transform: uppercase; |
|
||||
font-weight: 200; |
|
||||
padding: 10px 0; |
|
||||
border-bottom: 1px solid $grey; |
|
||||
color: $darkestgrey; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.peer { |
|
||||
position: relative; |
|
||||
background: $white; |
|
||||
padding: 10px; |
|
||||
border-top: 1px solid $grey; |
|
||||
cursor: pointer; |
|
||||
transition: all 0.25s; |
|
||||
|
|
||||
&:hover { |
|
||||
opacity: 0.5; |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border: none; |
|
||||
} |
|
||||
|
|
||||
h4, h1 { |
|
||||
margin: 10px 0; |
|
||||
} |
|
||||
|
|
||||
h4 { |
|
||||
font-size: 12px; |
|
||||
font-weight: bold; |
|
||||
color: $black; |
|
||||
} |
|
||||
|
|
||||
h1 { |
|
||||
font-size: 14px; |
|
||||
font-weight: 200; |
|
||||
color: $main; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.buttonGroup { |
|
||||
width: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
border-radius: 6px; |
|
||||
overflow: hidden; |
|
||||
|
|
||||
.button { |
|
||||
cursor: pointer; |
|
||||
height: 55px; |
|
||||
min-height: 55px; |
|
||||
text-transform: none; |
|
||||
font-size: 18px; |
|
||||
transition: opacity .2s ease-out; |
|
||||
background: $main; |
|
||||
color: $white; |
|
||||
border: none; |
|
||||
font-weight: 500; |
|
||||
padding: 0; |
|
||||
width: 100%; |
|
||||
text-align: center; |
|
||||
line-height: 55px; |
|
||||
|
|
||||
&:first-child { |
|
||||
border-right: 1px solid lighten($main, 20%); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,101 +0,0 @@ |
|||||
import { shell } from 'electron' |
|
||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import ReactModal from 'react-modal' |
|
||||
import styles from './ChannelModal.scss' |
|
||||
|
|
||||
const ChannelModal = ({ isOpen, resetChannel, channel, explorerLinkBase, closeChannel }) => { |
|
||||
const customStyles = { |
|
||||
overlay: { |
|
||||
cursor: 'pointer', |
|
||||
overflowY: 'auto' |
|
||||
}, |
|
||||
content: { |
|
||||
top: 'auto', |
|
||||
left: '20%', |
|
||||
right: '0', |
|
||||
bottom: 'auto', |
|
||||
width: '40%', |
|
||||
margin: '50px auto', |
|
||||
padding: '40px' |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const closeChannelClicked = () => { |
|
||||
closeChannel({ channel_point: channel.channel_point }) |
|
||||
resetChannel(null) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<ReactModal |
|
||||
isOpen={isOpen} |
|
||||
contentLabel='No Overlay Click Modal' |
|
||||
ariaHideApp |
|
||||
shouldCloseOnOverlayClick |
|
||||
onRequestClose={() => resetChannel(null)} |
|
||||
parentSelector={() => document.body} |
|
||||
style={customStyles} |
|
||||
> |
|
||||
{ |
|
||||
channel ? |
|
||||
<div className={styles.channel}> |
|
||||
<header className={styles.header}> |
|
||||
<h1 data-hint='Remote public key' className='hint--top-left'>{channel.remote_pubkey}</h1> |
|
||||
<h2 |
|
||||
data-hint='Channel point' |
|
||||
className='hint--top-left' |
|
||||
onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${channel.channel_point.split(':')[0]}`)} |
|
||||
> |
|
||||
{channel.channel_point} |
|
||||
</h2> |
|
||||
</header> |
|
||||
|
|
||||
<div className={styles.balances}> |
|
||||
<section className={styles.capacity}> |
|
||||
<h3>{channel.capacity}</h3> |
|
||||
<span>Capacity</span> |
|
||||
</section> |
|
||||
<div className={styles.balance}> |
|
||||
<section className={styles.local}> |
|
||||
<h4>{channel.local_balance}</h4> |
|
||||
<span>Local</span> |
|
||||
</section> |
|
||||
<section className={styles.remote}> |
|
||||
<h4>{channel.remote_balance}</h4> |
|
||||
<span>Remote</span> |
|
||||
</section> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className={styles.details}> |
|
||||
<dl> |
|
||||
<dt>Sent</dt> |
|
||||
<dd>{channel.total_satoshis_sent}</dd> |
|
||||
<dt>Received</dt> |
|
||||
<dd>{channel.total_satoshis_received}</dd> |
|
||||
<dt>Updates</dt> |
|
||||
<dd>{channel.num_updates}</dd> |
|
||||
</dl> |
|
||||
</div> |
|
||||
<div className={styles.close} onClick={closeChannelClicked}> |
|
||||
<div>Close channel</div> |
|
||||
</div> |
|
||||
<footer className={styles.active}> |
|
||||
<p>{channel.active ? 'Active' : 'Not active'}</p> |
|
||||
</footer> |
|
||||
</div> |
|
||||
: |
|
||||
null |
|
||||
} |
|
||||
</ReactModal> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
ChannelModal.propTypes = { |
|
||||
isOpen: PropTypes.bool.isRequired, |
|
||||
resetChannel: PropTypes.func.isRequired, |
|
||||
channel: PropTypes.object, |
|
||||
explorerLinkBase: PropTypes.string.isRequired, |
|
||||
closeChannel: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default ChannelModal |
|
@ -1,124 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.modalChannel { |
|
||||
padding: 40px; |
|
||||
} |
|
||||
|
|
||||
.header { |
|
||||
margin-bottom: 50px; |
|
||||
|
|
||||
h1 { |
|
||||
color: $black; |
|
||||
text-align: center; |
|
||||
margin-bottom: 5px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
h2 { |
|
||||
color: $darkestgrey; |
|
||||
font-size: 14px; |
|
||||
text-align: center; |
|
||||
|
|
||||
&:hover { |
|
||||
color: $main; |
|
||||
text-decoration: underline; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.balances { |
|
||||
.capacity { |
|
||||
text-align: center; |
|
||||
align-items: center; |
|
||||
|
|
||||
h3 { |
|
||||
color: $main; |
|
||||
font-size: 40px; |
|
||||
} |
|
||||
|
|
||||
span { |
|
||||
color: $black; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.balance { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
|
|
||||
.local, .remote { |
|
||||
flex: 5; |
|
||||
padding: 10px 30px; |
|
||||
text-align: center; |
|
||||
|
|
||||
h4 { |
|
||||
font-size: 20px; |
|
||||
color: $main; |
|
||||
} |
|
||||
|
|
||||
span { |
|
||||
color: $black; |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.details { |
|
||||
width: 75%; |
|
||||
margin: 20px auto; |
|
||||
|
|
||||
dt { |
|
||||
text-align: left; |
|
||||
float: left; |
|
||||
clear: left; |
|
||||
font-weight: 500; |
|
||||
padding: 20px 35px 19px 0; |
|
||||
color: $black; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
dd { |
|
||||
text-align: right; |
|
||||
font-weight: 400; |
|
||||
padding: 19px 0; |
|
||||
margin-left: 0; |
|
||||
border-top: 1px solid $darkgrey; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.close { |
|
||||
text-align: center; |
|
||||
|
|
||||
div { |
|
||||
width: 35%; |
|
||||
margin: 0 auto; |
|
||||
cursor: pointer; |
|
||||
height: 55px; |
|
||||
min-height: 55px; |
|
||||
text-transform: none; |
|
||||
font-size: 18px; |
|
||||
transition: opacity .2s ease-out; |
|
||||
background: $red; |
|
||||
color: $white; |
|
||||
border: none; |
|
||||
font-weight: 500; |
|
||||
padding: 0; |
|
||||
text-align: center; |
|
||||
line-height: 55px; |
|
||||
transition: all 0.25s; |
|
||||
border-radius: 5px; |
|
||||
|
|
||||
&:hover { |
|
||||
background: darken($red, 10%); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.active { |
|
||||
color: $darkestgrey; |
|
||||
text-align: center; |
|
||||
margin-top: 50px; |
|
||||
text-transform: uppercase; |
|
||||
} |
|
@ -1,140 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import { TiPlus } from 'react-icons/lib/ti' |
|
||||
import { FaRepeat } from 'react-icons/lib/fa' |
|
||||
import ChannelModal from './ChannelModal' |
|
||||
import ChannelForm from './ChannelForm' |
|
||||
import Channel from './Channel' |
|
||||
import OpenPendingChannel from './OpenPendingChannel' |
|
||||
import ClosedPendingChannel from './ClosedPendingChannel' |
|
||||
import styles from './Channels.scss' |
|
||||
|
|
||||
const Channels = ({ |
|
||||
fetchChannels, |
|
||||
ticker, |
|
||||
peers, |
|
||||
channelsLoading, |
|
||||
modalChannel, |
|
||||
setChannel, |
|
||||
channelModalOpen, |
|
||||
channelForm, |
|
||||
setChannelForm, |
|
||||
allChannels, |
|
||||
openChannel, |
|
||||
closeChannel, |
|
||||
currentTicker, |
|
||||
explorerLinkBase |
|
||||
}) => { |
|
||||
const refreshClicked = (event) => { |
|
||||
// store event in icon so we dont get an error when react clears it
|
|
||||
const icon = event.currentTarget |
|
||||
|
|
||||
// fetch channels
|
|
||||
fetchChannels() |
|
||||
|
|
||||
// clear animation after the second so we can reuse it
|
|
||||
setTimeout(() => { icon.style.animation = '' }, 1000) |
|
||||
|
|
||||
// spin icon for 1 sec
|
|
||||
icon.style.animation = 'spin 1000ms linear 1' |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className={styles.channels}> |
|
||||
<ChannelModal |
|
||||
isOpen={channelModalOpen} |
|
||||
resetChannel={setChannel} |
|
||||
channel={modalChannel} |
|
||||
explorerLinkBase={explorerLinkBase} |
|
||||
closeChannel={closeChannel} |
|
||||
/> |
|
||||
<ChannelForm |
|
||||
form={channelForm} |
|
||||
setForm={setChannelForm} |
|
||||
ticker={ticker} |
|
||||
peers={peers} |
|
||||
openChannel={openChannel} |
|
||||
currentTicker={currentTicker} |
|
||||
/> |
|
||||
<div className={styles.header}> |
|
||||
<h3>Channels</h3> |
|
||||
<span |
|
||||
className={`${styles.refresh} hint--top`} |
|
||||
data-hint='Refresh your channels list' |
|
||||
|
|
||||
> |
|
||||
<FaRepeat |
|
||||
style={{ verticalAlign: 'baseline' }} |
|
||||
onClick={refreshClicked} |
|
||||
/> |
|
||||
</span> |
|
||||
<div |
|
||||
className={`${styles.openChannel} hint--top`} |
|
||||
data-hint='Open a channel' |
|
||||
onClick={() => setChannelForm({ isOpen: true })} |
|
||||
> |
|
||||
<TiPlus /> |
|
||||
</div> |
|
||||
</div> |
|
||||
<ul> |
|
||||
{ |
|
||||
!channelsLoading ? |
|
||||
allChannels.map((channel, index) => { |
|
||||
if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) { |
|
||||
return ( |
|
||||
<OpenPendingChannel |
|
||||
key={index} |
|
||||
channel={channel} |
|
||||
ticker={ticker} |
|
||||
currentTicker={currentTicker} |
|
||||
explorerLinkBase={explorerLinkBase} |
|
||||
/> |
|
||||
) |
|
||||
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { |
|
||||
return ( |
|
||||
<ClosedPendingChannel |
|
||||
key={index} |
|
||||
channel={channel} |
|
||||
ticker={ticker} |
|
||||
currentTicker={currentTicker} |
|
||||
explorerLinkBase={explorerLinkBase} |
|
||||
/> |
|
||||
) |
|
||||
} |
|
||||
return ( |
|
||||
<Channel |
|
||||
key={index} |
|
||||
ticker={ticker} |
|
||||
channel={channel} |
|
||||
setChannel={setChannel} |
|
||||
currentTicker={currentTicker} |
|
||||
closeChannel={closeChannel} |
|
||||
/> |
|
||||
) |
|
||||
}) |
|
||||
: |
|
||||
'Loading...' |
|
||||
} |
|
||||
</ul> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Channels.propTypes = { |
|
||||
fetchChannels: PropTypes.func.isRequired, |
|
||||
ticker: PropTypes.object.isRequired, |
|
||||
peers: PropTypes.array.isRequired, |
|
||||
channelsLoading: PropTypes.bool.isRequired, |
|
||||
modalChannel: PropTypes.object, |
|
||||
setChannel: PropTypes.func.isRequired, |
|
||||
channelModalOpen: PropTypes.bool.isRequired, |
|
||||
channelForm: PropTypes.object.isRequired, |
|
||||
setChannelForm: PropTypes.func.isRequired, |
|
||||
allChannels: PropTypes.array.isRequired, |
|
||||
openChannel: PropTypes.func.isRequired, |
|
||||
closeChannel: PropTypes.func.isRequired, |
|
||||
currentTicker: PropTypes.object.isRequired, |
|
||||
explorerLinkBase: PropTypes.string.isRequired |
|
||||
} |
|
||||
|
|
||||
export default Channels |
|
@ -1,69 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
@keyframes spin { |
|
||||
from { |
|
||||
transform: rotate(0deg) |
|
||||
} |
|
||||
|
|
||||
to { |
|
||||
transform: rotate(360deg); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.channels { |
|
||||
width: 75%; |
|
||||
margin: 50px auto; |
|
||||
|
|
||||
.header { |
|
||||
margin-bottom: 10px; |
|
||||
|
|
||||
h3, .openChannel { |
|
||||
display: inline-block; |
|
||||
} |
|
||||
|
|
||||
h3 { |
|
||||
text-align: left; |
|
||||
} |
|
||||
|
|
||||
.refresh { |
|
||||
cursor: pointer; |
|
||||
margin-left: 5px; |
|
||||
font-size: 12px; |
|
||||
vertical-align: top; |
|
||||
color: $darkestgrey; |
|
||||
line-height: 14px; |
|
||||
transition: color 0.25s; |
|
||||
|
|
||||
&:hover { |
|
||||
color: $main; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.openChannel { |
|
||||
float: right; |
|
||||
cursor: pointer; |
|
||||
|
|
||||
svg { |
|
||||
padding: 3px; |
|
||||
border-radius: 50%; |
|
||||
border: 1px solid $main; |
|
||||
color: $main; |
|
||||
transition: all 0.25s; |
|
||||
|
|
||||
&:hover { |
|
||||
border-color: darken($main, 10%); |
|
||||
color: darken($main, 10%); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
h3 { |
|
||||
text-transform: uppercase; |
|
||||
color: $darkestgrey; |
|
||||
letter-spacing: 1.6px; |
|
||||
font-size: 14px; |
|
||||
font-weight: 400; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
} |
|
@ -1,67 +0,0 @@ |
|||||
import { shell } from 'electron' |
|
||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import { btc } from 'utils' |
|
||||
import styles from './ClosedPendingChannel.scss' |
|
||||
|
|
||||
const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid }, currentTicker, explorerLinkBase }) => ( |
|
||||
<li className={styles.channel} onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${closing_txid}`)}> |
|
||||
<h1 className={styles.closing}>Closing Channel...</h1> |
|
||||
<div className={styles.left}> |
|
||||
<section className={styles.remotePubkey}> |
|
||||
<span>Remote Pubkey</span> |
|
||||
<h4>{channel.remote_node_pub}</h4> |
|
||||
</section> |
|
||||
<section className={styles.channelPoint}> |
|
||||
<span>Channel Point</span> |
|
||||
<h4>{channel.channel_point}</h4> |
|
||||
</section> |
|
||||
</div> |
|
||||
<div className={styles.right}> |
|
||||
<section className={styles.capacity}> |
|
||||
<span>Capacity</span> |
|
||||
<h2> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.capacity) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd) |
|
||||
} |
|
||||
</h2> |
|
||||
</section> |
|
||||
<div className={styles.balances}> |
|
||||
<section> |
|
||||
<h4> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.local_balance) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd) |
|
||||
} |
|
||||
</h4> |
|
||||
<span>Local</span> |
|
||||
</section> |
|
||||
<section> |
|
||||
<h4> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.remote_balance) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd) |
|
||||
} |
|
||||
</h4> |
|
||||
<span>Remote</span> |
|
||||
</section> |
|
||||
</div> |
|
||||
</div> |
|
||||
</li> |
|
||||
) |
|
||||
|
|
||||
ClosedPendingChannel.propTypes = { |
|
||||
ticker: PropTypes.object.isRequired, |
|
||||
channel: PropTypes.object.isRequired, |
|
||||
currentTicker: PropTypes.object.isRequired, |
|
||||
explorerLinkBase: PropTypes.string.isRequired |
|
||||
} |
|
||||
|
|
||||
export default ClosedPendingChannel |
|
@ -1,95 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.channel { |
|
||||
position: relative; |
|
||||
background: $white; |
|
||||
padding: 10px; |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
border-top: 1px solid $grey; |
|
||||
cursor: pointer; |
|
||||
transition: all 0.25s; |
|
||||
opacity: 0.5; |
|
||||
|
|
||||
&:hover { |
|
||||
opacity: 0.35; |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border: none; |
|
||||
} |
|
||||
|
|
||||
.closing { |
|
||||
color: $red; |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 10px; |
|
||||
padding: 10px; |
|
||||
text-transform: uppercase; |
|
||||
font-weight: bold; |
|
||||
font-size: 10px; |
|
||||
} |
|
||||
|
|
||||
.left, .right { |
|
||||
padding: 0 10px; |
|
||||
margin-bottom: 5; |
|
||||
margin-top: 25px; |
|
||||
|
|
||||
section { |
|
||||
margin-bottom: 20px; |
|
||||
|
|
||||
span { |
|
||||
text-transform: uppercase; |
|
||||
letter-spacing: 1.6px; |
|
||||
color: $black; |
|
||||
font-size: 10px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
h2 { |
|
||||
font-size: 30px; |
|
||||
padding: 5px 0; |
|
||||
color: $main; |
|
||||
} |
|
||||
|
|
||||
h4 { |
|
||||
margin-top: 5px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.left { |
|
||||
flex: 7; |
|
||||
border-right: 1px solid $grey; |
|
||||
} |
|
||||
|
|
||||
.right { |
|
||||
flex: 3; |
|
||||
|
|
||||
.capacity { |
|
||||
text-align: center; |
|
||||
border-bottom: 1px solid $grey; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.balances { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
|
|
||||
section { |
|
||||
flex: 5; |
|
||||
text-align: center; |
|
||||
|
|
||||
h4 { |
|
||||
color: $main; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border-right: 1px solid $grey; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,70 +0,0 @@ |
|||||
import { shell } from 'electron' |
|
||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import { btc } from 'utils' |
|
||||
import styles from './OpenPendingChannel.scss' |
|
||||
|
|
||||
const OpenPendingChannel = ({ ticker, channel, currentTicker, explorerLinkBase }) => ( |
|
||||
<li className={styles.channel} onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${channel.channel.channel_point.split(':')[0]}`)}> |
|
||||
<div className={styles.pending}> |
|
||||
<h1>Opening Channel...</h1> |
|
||||
<span>Blocks till open: {channel.blocks_till_open}</span> |
|
||||
</div> |
|
||||
<div className={styles.left}> |
|
||||
<section className={styles.remotePubkey}> |
|
||||
<span>Remote Pubkey</span> |
|
||||
<h4>{channel.channel.remote_node_pub}</h4> |
|
||||
</section> |
|
||||
<section className={styles.channelPoint}> |
|
||||
<span>Channel Point</span> |
|
||||
<h4>{channel.channel.channel_point}</h4> |
|
||||
</section> |
|
||||
</div> |
|
||||
<div className={styles.right}> |
|
||||
<section className={styles.capacity}> |
|
||||
<span>Capacity</span> |
|
||||
<h2> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.channel.capacity) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.channel.capacity, currentTicker.price_usd) |
|
||||
} |
|
||||
</h2> |
|
||||
</section> |
|
||||
<div className={styles.balances}> |
|
||||
<section> |
|
||||
<h4> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.channel.local_balance) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.channel.local_balance, currentTicker.price_usd) |
|
||||
} |
|
||||
</h4> |
|
||||
<span>Local</span> |
|
||||
</section> |
|
||||
<section> |
|
||||
<h4> |
|
||||
{ |
|
||||
ticker.currency === 'btc' ? |
|
||||
btc.satoshisToBtc(channel.channel.remote_balance) |
|
||||
: |
|
||||
btc.satoshisToUsd(channel.channel.remote_balance, currentTicker.price_usd) |
|
||||
} |
|
||||
</h4> |
|
||||
<span>Remote</span> |
|
||||
</section> |
|
||||
</div> |
|
||||
</div> |
|
||||
</li> |
|
||||
) |
|
||||
|
|
||||
OpenPendingChannel.propTypes = { |
|
||||
ticker: PropTypes.object.isRequired, |
|
||||
channel: PropTypes.object.isRequired, |
|
||||
currentTicker: PropTypes.object.isRequired, |
|
||||
explorerLinkBase: PropTypes.string.isRequired |
|
||||
} |
|
||||
|
|
||||
export default OpenPendingChannel |
|
@ -1,98 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.channel { |
|
||||
position: relative; |
|
||||
background: $lightgrey; |
|
||||
padding: 10px; |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
border-top: 1px solid $grey; |
|
||||
cursor: pointer; |
|
||||
transition: all 0.25s; |
|
||||
opacity: 0.5; |
|
||||
|
|
||||
.pending { |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 10px; |
|
||||
padding: 10px; |
|
||||
text-transform: uppercase; |
|
||||
font-weight: bold; |
|
||||
|
|
||||
h1 { |
|
||||
color: $main; |
|
||||
font-size: 10px; |
|
||||
} |
|
||||
|
|
||||
span { |
|
||||
font-size: 8px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border: none; |
|
||||
} |
|
||||
|
|
||||
.left, .right { |
|
||||
padding: 0 10px; |
|
||||
margin-bottom: 5; |
|
||||
margin-top: 40px; |
|
||||
|
|
||||
section { |
|
||||
margin-bottom: 20px; |
|
||||
|
|
||||
span { |
|
||||
text-transform: uppercase; |
|
||||
letter-spacing: 1.6px; |
|
||||
color: $black; |
|
||||
font-size: 10px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
h2 { |
|
||||
font-size: 30px; |
|
||||
padding: 5px 0; |
|
||||
color: $main; |
|
||||
} |
|
||||
|
|
||||
h4 { |
|
||||
margin-top: 5px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.left { |
|
||||
flex: 7; |
|
||||
border-right: 1px solid $grey; |
|
||||
} |
|
||||
|
|
||||
.right { |
|
||||
flex: 3; |
|
||||
|
|
||||
.capacity { |
|
||||
text-align: center; |
|
||||
border-bottom: 1px solid $grey; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.balances { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
|
|
||||
section { |
|
||||
flex: 5; |
|
||||
text-align: center; |
|
||||
|
|
||||
h4 { |
|
||||
color: $main; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border-right: 1px solid $grey; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,3 +0,0 @@ |
|||||
import Channels from './Channels' |
|
||||
|
|
||||
export default Channels |
|
@ -0,0 +1,39 @@ |
|||||
|
import { shell } from 'electron' |
||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { FaCircle } from 'react-icons/lib/fa' |
||||
|
import { btc } from 'utils' |
||||
|
import styles from './Contact.scss' |
||||
|
|
||||
|
const ClosingContact = ({ channel }) => ( |
||||
|
<li className={styles.friend}> |
||||
|
<section className={styles.info}> |
||||
|
<p className={styles.closing}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> |
||||
|
<span> |
||||
|
Removing |
||||
|
<i onClick={() => shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.closing_txid}`)}> |
||||
|
(Details) |
||||
|
</i> |
||||
|
</span> |
||||
|
</p> |
||||
|
<h2>{channel.channel.remote_node_pub}</h2> |
||||
|
</section> |
||||
|
<section className={styles.limits}> |
||||
|
<div> |
||||
|
<h4>Can Pay</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.channel.local_balance)}BTC</p> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h4>Can Receive</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.channel.remote_balance)}BTC</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
</li> |
||||
|
) |
||||
|
|
||||
|
ClosingContact.propTypes = { |
||||
|
channel: PropTypes.object.isRequired |
||||
|
} |
||||
|
|
||||
|
export default ClosingContact |
@ -0,0 +1,156 @@ |
|||||
|
@import '../../variables.scss'; |
||||
|
|
||||
|
.friend { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
padding: 30px 60px 60px 60px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.25s; |
||||
|
|
||||
|
&.loading { |
||||
|
.info { |
||||
|
opacity: 0.2; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background: $lightgrey; |
||||
|
} |
||||
|
|
||||
|
.limits { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
|
||||
|
div { |
||||
|
margin: 0 10px; |
||||
|
|
||||
|
h4 { |
||||
|
font-size: 12px; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.info { |
||||
|
p { |
||||
|
margin-bottom: 20px; |
||||
|
|
||||
|
&.online { |
||||
|
color: $green; |
||||
|
|
||||
|
svg { |
||||
|
color: $green; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.pending { |
||||
|
color: $orange; |
||||
|
|
||||
|
svg { |
||||
|
color: $orange; |
||||
|
} |
||||
|
|
||||
|
i { |
||||
|
margin-left: 5px; |
||||
|
color: $darkestgrey; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.closing { |
||||
|
color: $red; |
||||
|
|
||||
|
svg { |
||||
|
color: $red; |
||||
|
} |
||||
|
|
||||
|
i { |
||||
|
margin-left: 5px; |
||||
|
color: $darkestgrey; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
svg, span { |
||||
|
display: inline-block; |
||||
|
vertical-align: top; |
||||
|
} |
||||
|
|
||||
|
svg { |
||||
|
margin-right: 5px; |
||||
|
width: 12px; |
||||
|
height: 12px; |
||||
|
color: $darkestgrey; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
color: $black; |
||||
|
font-size: 14px; |
||||
|
font-weight: bold; |
||||
|
letter-spacing: 1.3px; |
||||
|
|
||||
|
span { |
||||
|
color: $darkestgrey; |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@-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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
border: 1px solid rgba(0, 0, 0, 0.1); |
||||
|
border-left-color: rgba(0, 0, 0, 0.4); |
||||
|
-webkit-border-radius: 999px; |
||||
|
-moz-border-radius: 999px; |
||||
|
border-radius: 999px; |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
margin: 0 auto; |
||||
|
height: 50px; |
||||
|
width: 50px; |
||||
|
-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; |
||||
|
} |
||||
|
|
@ -0,0 +1,134 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import find from 'lodash/find' |
||||
|
import ReactModal from 'react-modal' |
||||
|
import { FaClose, FaCircle } from 'react-icons/lib/fa' |
||||
|
|
||||
|
import { btc } from 'utils' |
||||
|
|
||||
|
import styles from './ContactModal.scss' |
||||
|
|
||||
|
const ContactModal = ({ |
||||
|
isOpen, |
||||
|
channel, |
||||
|
closeContactModal, |
||||
|
channelNodes, |
||||
|
closeChannel, |
||||
|
closingChannelIds |
||||
|
}) => { |
||||
|
if (!channel) { return <span /> } |
||||
|
|
||||
|
const customStyles = { |
||||
|
overlay: { |
||||
|
cursor: 'pointer', |
||||
|
overflowY: 'auto' |
||||
|
}, |
||||
|
content: { |
||||
|
top: 'auto', |
||||
|
left: '20%', |
||||
|
right: '0', |
||||
|
bottom: 'auto', |
||||
|
width: '40%', |
||||
|
margin: '50px auto', |
||||
|
borderRadius: 'none', |
||||
|
padding: '0' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const removeClicked = () => { |
||||
|
closeChannel({ channel_point: channel.channel_point, chan_id: channel.chan_id, force: !channel.active }) |
||||
|
} |
||||
|
|
||||
|
// the remote node for the channel
|
||||
|
const node = find(channelNodes, { pub_key: channel.remote_pubkey }) |
||||
|
|
||||
|
return ( |
||||
|
<ReactModal |
||||
|
isOpen={isOpen} |
||||
|
contentLabel='No Overlay Click Modal' |
||||
|
ariaHideApp |
||||
|
shouldCloseOnOverlayClick |
||||
|
onRequestClose={closeContactModal} |
||||
|
parentSelector={() => document.body} |
||||
|
style={customStyles} |
||||
|
> |
||||
|
{ |
||||
|
channel && |
||||
|
<div className={styles.container}> |
||||
|
<header className={styles.header}> |
||||
|
<div className={`${styles.status} ${channel.active && styles.online}`}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> |
||||
|
<span> |
||||
|
{channel.active ? 'Online' : 'Offline'} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className={styles.closeContainer}> |
||||
|
<span onClick={closeContactModal}> |
||||
|
<FaClose /> |
||||
|
</span> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<section className={styles.title}> |
||||
|
{ |
||||
|
node && |
||||
|
<h1>{node.alias}</h1> |
||||
|
} |
||||
|
<h2>{channel.remote_pubkey}</h2> |
||||
|
</section> |
||||
|
|
||||
|
<section className={styles.stats}> |
||||
|
<div className={styles.pay}> |
||||
|
<h4>Can Pay</h4> |
||||
|
<div className={styles.meter}> |
||||
|
<div className={styles.amount} style={{ width: `${(channel.local_balance / channel.capacity) * 100}%` }} /> |
||||
|
</div> |
||||
|
<span>{btc.satoshisToBtc(channel.local_balance)} BTC</span> |
||||
|
</div> |
||||
|
|
||||
|
<div className={styles.pay}> |
||||
|
<h4>Can Receive</h4> |
||||
|
<div className={styles.meter}> |
||||
|
<div className={styles.amount} style={{ width: `${(channel.remote_balance / channel.capacity) * 100}%` }} /> |
||||
|
</div> |
||||
|
<span>{btc.satoshisToBtc(channel.remote_balance)} BTC</span> |
||||
|
</div> |
||||
|
|
||||
|
<div className={styles.sent}> |
||||
|
<h4>Total Bitcoin Sent</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.total_satoshis_sent)} BTC</p> |
||||
|
</div> |
||||
|
<div className={styles.received}> |
||||
|
<h4>Total Bitcoin Received</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.total_satoshis_received)} BTC</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<footer> |
||||
|
{ |
||||
|
closingChannelIds.includes(channel.chan_id) ? |
||||
|
<span className={styles.inactive}> |
||||
|
<div className={styles.loading}> |
||||
|
<div className={styles.spinner} /> |
||||
|
</div> |
||||
|
</span> |
||||
|
: |
||||
|
<div onClick={removeClicked}>Remove</div> |
||||
|
} |
||||
|
</footer> |
||||
|
</div> |
||||
|
} |
||||
|
</ReactModal> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
ContactModal.propTypes = { |
||||
|
channel: PropTypes.object, |
||||
|
isOpen: PropTypes.bool.isRequired, |
||||
|
closeContactModal: PropTypes.func.isRequired, |
||||
|
channelNodes: PropTypes.array.isRequired, |
||||
|
closeChannel: PropTypes.func.isRequired, |
||||
|
closingChannelIds: PropTypes.array.isRequired |
||||
|
} |
||||
|
|
||||
|
export default ContactModal |
@ -0,0 +1,152 @@ |
|||||
|
@import '../../variables.scss'; |
||||
|
|
||||
|
.header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: flex-end; |
||||
|
background: $lightgrey; |
||||
|
padding: 20px; |
||||
|
|
||||
|
.status { |
||||
|
font-size: 12px; |
||||
|
color: $darkestgrey; |
||||
|
|
||||
|
&.online { |
||||
|
color: $green; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.closeContainer { |
||||
|
background: $lightgrey; |
||||
|
line-height: 12px; |
||||
|
|
||||
|
span { |
||||
|
color: $darkestgrey; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.container section { |
||||
|
margin-bottom: 30px; |
||||
|
padding: 0 20px; |
||||
|
|
||||
|
.pay, .receive, .sent, .received { |
||||
|
margin: 40px 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.container .title { |
||||
|
margin: 0; |
||||
|
padding: 30px 20px; |
||||
|
background: $lightgrey; |
||||
|
|
||||
|
h1 { |
||||
|
color: $secondary; |
||||
|
font-weight: bold; |
||||
|
font-size: 16px; |
||||
|
letter-spacing: 1.1px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 12px; |
||||
|
color: $darkestgrey; |
||||
|
font-weight: 100; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.stats { |
||||
|
|
||||
|
h4 { |
||||
|
color: $secondary; |
||||
|
font-weight: bold; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
margin-top: 10px; |
||||
|
color: $darkestgrey; |
||||
|
} |
||||
|
|
||||
|
.meter, .amount { |
||||
|
height: 10px; |
||||
|
border-radius: 10px; |
||||
|
} |
||||
|
|
||||
|
.meter { |
||||
|
background: $darkgrey; |
||||
|
width: 100%; |
||||
|
margin: 10px 0; |
||||
|
} |
||||
|
|
||||
|
.amount { |
||||
|
background: $darkestgrey; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.container footer { |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
|
||||
|
div { |
||||
|
color: $red; |
||||
|
font-size: 18px; |
||||
|
|
||||
|
&:hover { |
||||
|
color: lighten($red, 10%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@-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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
border: 1px solid rgba(0, 0, 0, 0.1); |
||||
|
border-left-color: rgba(0, 0, 0, 0.4); |
||||
|
-webkit-border-radius: 999px; |
||||
|
-moz-border-radius: 999px; |
||||
|
border-radius: 999px; |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
margin: 0 auto; |
||||
|
height: 20px; |
||||
|
width: 20px; |
||||
|
-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; |
||||
|
} |
@ -0,0 +1,230 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import ReactModal from 'react-modal' |
||||
|
import { MdClose } from 'react-icons/lib/md' |
||||
|
import { FaCircle, FaQuestionCircle } from 'react-icons/lib/fa' |
||||
|
import styles from './ContactsForm.scss' |
||||
|
|
||||
|
class ContactsForm extends React.Component { |
||||
|
constructor(props) { |
||||
|
super(props) |
||||
|
|
||||
|
this.state = { |
||||
|
editing: false, |
||||
|
manualFormInput: '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
contactsform, |
||||
|
closeContactsForm, |
||||
|
updateContactFormSearchQuery, |
||||
|
updateContactCapacity, |
||||
|
openChannel, |
||||
|
|
||||
|
activeChannelPubkeys, |
||||
|
nonActiveChannelPubkeys, |
||||
|
pendingOpenChannelPubkeys, |
||||
|
filteredNetworkNodes, |
||||
|
loadingChannelPubkeys, |
||||
|
showManualForm |
||||
|
} = this.props |
||||
|
|
||||
|
const { editing, manualFormInput } = this.state |
||||
|
|
||||
|
|
||||
|
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}`}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Online</span> |
||||
|
</span> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (nonActiveChannelPubkeys.includes(node.pub_key)) { |
||||
|
return ( |
||||
|
<span className={`${styles.offline} ${styles.inactive}`}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Offline</span> |
||||
|
</span> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (pendingOpenChannelPubkeys.includes(node.pub_key)) { |
||||
|
return ( |
||||
|
<span className={`${styles.pending} ${styles.inactive}`}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Pending</span> |
||||
|
</span> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (!node.addresses.length) { |
||||
|
return ( |
||||
|
<span className={`${styles.private} ${styles.inactive}`}> |
||||
|
Private |
||||
|
</span> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<span |
||||
|
className={`${styles.connect} hint--left`} |
||||
|
data-hint={`Connect with ${contactsform.contactCapacity} BTC`} |
||||
|
onClick={() => openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: contactsform.contactCapacity })} |
||||
|
> |
||||
|
Connect |
||||
|
</span> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const inputClicked = () => { |
||||
|
if (editing) { return } |
||||
|
|
||||
|
this.setState({ editing: true }) |
||||
|
} |
||||
|
|
||||
|
const manualFormSubmit = () => { |
||||
|
if (!manualFormInput.length) { return } |
||||
|
if (!manualFormInput.includes('@')) { return } |
||||
|
|
||||
|
const [pubkey, host] = manualFormInput && manualFormInput.split('@') |
||||
|
|
||||
|
openChannel({ pubkey, host, local_amt: contactsform.contactCapacity }) |
||||
|
|
||||
|
this.setState({ manualFormInput: '' }) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<ReactModal |
||||
|
isOpen={contactsform.isOpen} |
||||
|
contentLabel='No Overlay Click Modal' |
||||
|
ariaHideApp |
||||
|
shouldCloseOnOverlayClick |
||||
|
onRequestClose={() => closeContactsForm} |
||||
|
parentSelector={() => document.body} |
||||
|
className={styles.modal} |
||||
|
> |
||||
|
<header> |
||||
|
<div> |
||||
|
<h1>Add Contact</h1> |
||||
|
</div> |
||||
|
<div onClick={closeContactsForm} className={styles.modalClose}> |
||||
|
<MdClose /> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<div className={styles.form}> |
||||
|
<div className={styles.search}> |
||||
|
<input |
||||
|
type='text' |
||||
|
placeholder='Find contact by alias or pubkey' |
||||
|
className={styles.searchInput} |
||||
|
value={contactsform.searchQuery} |
||||
|
onChange={event => updateContactFormSearchQuery(event.target.value)} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<ul className={styles.networkResults}> |
||||
|
{ |
||||
|
contactsform.searchQuery.length > 0 && 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> |
||||
|
</div> |
||||
|
|
||||
|
{ |
||||
|
showManualForm && |
||||
|
<div className={styles.manualForm}> |
||||
|
<h2>Hm, looks like we can’t see that contact from here. Want to try and manually connect?</h2> |
||||
|
<section> |
||||
|
<input |
||||
|
type='text' |
||||
|
placeholder='pubkey@host' |
||||
|
value={manualFormInput} |
||||
|
onChange={event => this.setState({ manualFormInput: event.target.value })} |
||||
|
/> |
||||
|
<div className={styles.submit} onClick={manualFormSubmit}>Submit</div> |
||||
|
</section> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<footer className={styles.footer}> |
||||
|
<div> |
||||
|
<span> |
||||
|
Use |
||||
|
</span> |
||||
|
<span className={styles.amount}> |
||||
|
<input |
||||
|
type='text' |
||||
|
value={contactsform.contactCapacity} |
||||
|
onChange={event => updateContactCapacity(event.target.value)} |
||||
|
onClick={inputClicked} |
||||
|
onKeyPress={event => event.charCode === 13 && this.setState({ editing: false })} |
||||
|
readOnly={!editing} |
||||
|
style={{ width: `${editing ? 20 : contactsform.contactCapacity.toString().length + 1}%` }} |
||||
|
/> |
||||
|
</span> |
||||
|
<span className={styles.caption}> |
||||
|
BTC per contact |
||||
|
<i |
||||
|
data-hint="You aren't spending anything, just moving money onto the Lightning Network" |
||||
|
className='hint--top' |
||||
|
> |
||||
|
<FaQuestionCircle style={{ verticalAlign: 'top' }} /> |
||||
|
</i> |
||||
|
</span> |
||||
|
</div> |
||||
|
</footer> |
||||
|
</ReactModal> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ContactsForm.propTypes = { |
||||
|
contactsform: PropTypes.object.isRequired, |
||||
|
closeContactsForm: PropTypes.func.isRequired, |
||||
|
updateContactFormSearchQuery: PropTypes.func.isRequired, |
||||
|
updateContactCapacity: PropTypes.func.isRequired, |
||||
|
openChannel: 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 |
||||
|
} |
||||
|
|
||||
|
export default ContactsForm |
@ -0,0 +1,239 @@ |
|||||
|
@import '../../variables.scss'; |
||||
|
|
||||
|
.modal { |
||||
|
position: relative; |
||||
|
width: 50%; |
||||
|
margin: 50px auto; |
||||
|
position: absolute; |
||||
|
top: auto; |
||||
|
left: 20%; |
||||
|
right: 0; |
||||
|
bottom: auto; |
||||
|
background: $white; |
||||
|
outline: none; |
||||
|
z-index: -2; |
||||
|
border: 1px solid $darkgrey; |
||||
|
|
||||
|
header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
padding: 15px; |
||||
|
border-bottom: 1px solid $darkgrey; |
||||
|
|
||||
|
h1, svg { |
||||
|
font-size: 22px; |
||||
|
} |
||||
|
|
||||
|
svg { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.form { |
||||
|
padding: 30px 15px; |
||||
|
|
||||
|
.search { |
||||
|
.searchInput { |
||||
|
width: calc(100% - 30px); |
||||
|
padding: 10px 15px; |
||||
|
outline: 0; |
||||
|
border: 0; |
||||
|
background: $lightgrey; |
||||
|
color: $darkestgrey; |
||||
|
border-radius: 5px; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.networkResults { |
||||
|
overflow-y: scroll; |
||||
|
height: 300px; |
||||
|
margin-top: 30px; |
||||
|
padding: 20px 0; |
||||
|
|
||||
|
li { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
padding: 10px 0; |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 16px; |
||||
|
font-weight: bold; |
||||
|
letter-spacing: 1.3px; |
||||
|
|
||||
|
span { |
||||
|
display: inline-block; |
||||
|
vertical-align: middle; |
||||
|
|
||||
|
&:nth-child(1) { |
||||
|
font-size: 14px; |
||||
|
font-weight: bold; |
||||
|
letter-spacing: 1.3px; |
||||
|
} |
||||
|
|
||||
|
&:nth-child(2) { |
||||
|
color: $darkestgrey; |
||||
|
font-size: 12px; |
||||
|
line-height: 14px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.connect { |
||||
|
cursor: pointer; |
||||
|
color: $darkestgrey; |
||||
|
transition: all 0.25s; |
||||
|
font-size: 12px; |
||||
|
|
||||
|
&:hover { |
||||
|
color: $main; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.inactive { |
||||
|
font-size: 12px; |
||||
|
|
||||
|
display: inline-block; |
||||
|
vertical-align: top; |
||||
|
|
||||
|
&.online { |
||||
|
color: $green; |
||||
|
} |
||||
|
|
||||
|
&.offline { |
||||
|
color: $darkestgrey; |
||||
|
} |
||||
|
|
||||
|
&.pending { |
||||
|
color: $orange; |
||||
|
} |
||||
|
|
||||
|
&.private { |
||||
|
color: darken($darkestgrey, 50%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.manualForm { |
||||
|
background: $lightgrey; |
||||
|
color: $darkestgrey; |
||||
|
padding: 30px 15px; |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 16px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
input { |
||||
|
border: 0; |
||||
|
outline: 0; |
||||
|
background: transparent; |
||||
|
color: $darkestgrey; |
||||
|
border-bottom: 1px solid $darkestgrey; |
||||
|
padding: 10px 5px; |
||||
|
width: 80%; |
||||
|
} |
||||
|
|
||||
|
.submit { |
||||
|
display: inline-block; |
||||
|
vertical-align: middle; |
||||
|
width: 15%; |
||||
|
margin-left: 2.5%; |
||||
|
font-size: 12px; |
||||
|
|
||||
|
&:hover { |
||||
|
cursor: pointer; |
||||
|
color: $main; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.footer { |
||||
|
padding: 10px 15px; |
||||
|
border-top: 1px solid $darkgrey; |
||||
|
font-size: 14px; |
||||
|
|
||||
|
span { |
||||
|
&.amount { |
||||
|
&:hover { |
||||
|
input { |
||||
|
border: 1px solid $darkgrey; |
||||
|
cursor: text; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
input { |
||||
|
border: 1px solid transparent; |
||||
|
padding: 0; |
||||
|
outline: 0; |
||||
|
font-weight: bold; |
||||
|
font-size: 14px; |
||||
|
line-height: 14px; |
||||
|
transition: all 0.25s; |
||||
|
|
||||
|
&.isEditing { |
||||
|
width: 100%; |
||||
|
border-bottom: 1px solid $darkgrey; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:nth-child(2) { |
||||
|
margin-left: 2px; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.caption svg { |
||||
|
font-size: 10px; |
||||
|
color: $darkestgrey; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@-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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
border: 1px solid rgba(0, 0, 0, 0.1); |
||||
|
border-left-color: rgba(0, 0, 0, 0.4); |
||||
|
-webkit-border-radius: 999px; |
||||
|
-moz-border-radius: 999px; |
||||
|
border-radius: 999px; |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
margin: 0 auto; |
||||
|
height: 20px; |
||||
|
width: 20px; |
||||
|
-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; |
||||
|
} |
||||
|
|
@ -0,0 +1,35 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { FaCircle } from 'react-icons/lib/fa' |
||||
|
import styles from './Contact.scss' |
||||
|
|
||||
|
const LoadingContact = ({ pubkey, isClosing }) => ( |
||||
|
<li className={`${styles.friend} ${styles.loading}`}> |
||||
|
<section className={styles.info}> |
||||
|
<p> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> |
||||
|
<span> |
||||
|
{ |
||||
|
isClosing ? |
||||
|
'Closing' |
||||
|
: |
||||
|
'Loading' |
||||
|
} |
||||
|
</span> |
||||
|
</p> |
||||
|
<h2>{pubkey}</h2> |
||||
|
</section> |
||||
|
<section className={styles.limits}> |
||||
|
<div className={styles.loading}> |
||||
|
<div className={styles.spinner} /> |
||||
|
</div> |
||||
|
</section> |
||||
|
</li> |
||||
|
) |
||||
|
|
||||
|
LoadingContact.propTypes = { |
||||
|
pubkey: PropTypes.string.isRequired, |
||||
|
isClosing: PropTypes.bool.isRequired |
||||
|
} |
||||
|
|
||||
|
export default LoadingContact |
@ -0,0 +1,34 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { FaCircle } from 'react-icons/lib/fa' |
||||
|
import { btc } from 'utils' |
||||
|
import styles from './Contact.scss' |
||||
|
|
||||
|
const OfflineContact = ({ channel, openContactModal }) => ( |
||||
|
<li className={styles.friend} key={channel.chan_id} onClick={() => openContactModal(channel)}> |
||||
|
<section className={styles.info}> |
||||
|
<p> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> |
||||
|
<span>Offline</span> |
||||
|
</p> |
||||
|
<h2>{channel.remote_pubkey}</h2> |
||||
|
</section> |
||||
|
<section className={styles.limits}> |
||||
|
<div> |
||||
|
<h4>Can Pay</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.local_balance)}BTC</p> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h4>Can Receive</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.remote_balance)}BTC</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
</li> |
||||
|
) |
||||
|
|
||||
|
OfflineContact.propTypes = { |
||||
|
channel: PropTypes.object.isRequired, |
||||
|
openContactModal: PropTypes.func.isRequired |
||||
|
} |
||||
|
|
||||
|
export default OfflineContact |
@ -0,0 +1,34 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { FaCircle } from 'react-icons/lib/fa' |
||||
|
import { btc } from 'utils' |
||||
|
import styles from './Contact.scss' |
||||
|
|
||||
|
const OnlineContact = ({ channel, openContactModal }) => ( |
||||
|
<li className={styles.friend} key={channel.chan_id} onClick={() => openContactModal(channel)}> |
||||
|
<section className={styles.info}> |
||||
|
<p className={styles.online}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> |
||||
|
<span>Online</span> |
||||
|
</p> |
||||
|
<h2>{channel.remote_pubkey}</h2> |
||||
|
</section> |
||||
|
<section className={styles.limits}> |
||||
|
<div> |
||||
|
<h4>Can Pay</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.local_balance)}BTC</p> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h4>Can Receive</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.remote_balance)}BTC</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
</li> |
||||
|
) |
||||
|
|
||||
|
OnlineContact.propTypes = { |
||||
|
channel: PropTypes.object.isRequired, |
||||
|
openContactModal: PropTypes.func.isRequired |
||||
|
} |
||||
|
|
||||
|
export default OnlineContact |
@ -0,0 +1,39 @@ |
|||||
|
import { shell } from 'electron' |
||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { FaCircle } from 'react-icons/lib/fa' |
||||
|
import { btc } from 'utils' |
||||
|
import styles from './Contact.scss' |
||||
|
|
||||
|
const PendingContact = ({ channel }) => ( |
||||
|
<li className={styles.friend} key={channel.chan_id}> |
||||
|
<section className={styles.info}> |
||||
|
<p className={styles.pending}> |
||||
|
<FaCircle style={{ verticalAlign: 'top' }} /> |
||||
|
<span> |
||||
|
Pending |
||||
|
<i onClick={() => shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.channel.channel_point.split(':')[0]}`)}> |
||||
|
(Details) |
||||
|
</i> |
||||
|
</span> |
||||
|
</p> |
||||
|
<h2>{channel.channel.remote_node_pub}</h2> |
||||
|
</section> |
||||
|
<section className={styles.limits}> |
||||
|
<div> |
||||
|
<h4>Can Pay</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.channel.local_balance)}BTC</p> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h4>Can Receive</h4> |
||||
|
<p>{btc.satoshisToBtc(channel.channel.remote_balance)}BTC</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
</li> |
||||
|
) |
||||
|
|
||||
|
PendingContact.propTypes = { |
||||
|
channel: PropTypes.object.isRequired |
||||
|
} |
||||
|
|
||||
|
export default PendingContact |
@ -1,20 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import styles from './Peer.scss' |
|
||||
|
|
||||
const Peer = ({ peer, setPeer }) => ( |
|
||||
<li className={styles.peer} onClick={() => setPeer(peer)}> |
|
||||
<h4>{peer.address}</h4> |
|
||||
<h1>{peer.pub_key}</h1> |
|
||||
</li> |
|
||||
) |
|
||||
|
|
||||
Peer.propTypes = { |
|
||||
peer: PropTypes.shape({ |
|
||||
address: PropTypes.string.isRequired, |
|
||||
pub_key: PropTypes.string.isRequired |
|
||||
}).isRequired, |
|
||||
setPeer: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default Peer |
|
@ -1,38 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.peer { |
|
||||
position: relative; |
|
||||
margin: 5px 0; |
|
||||
padding: 10px; |
|
||||
border-top: 1px solid $white; |
|
||||
cursor: pointer; |
|
||||
transition: all 0.25s; |
|
||||
list-style: none; |
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); |
|
||||
transition: 0.3s; |
|
||||
|
|
||||
&:hover { |
|
||||
opacity: 0.75; |
|
||||
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); |
|
||||
} |
|
||||
|
|
||||
&:first-child { |
|
||||
border: none; |
|
||||
} |
|
||||
|
|
||||
h4, h1 { |
|
||||
margin: 10px 0; |
|
||||
} |
|
||||
|
|
||||
h4 { |
|
||||
font-size: 14px; |
|
||||
font-weight: bold; |
|
||||
color: $black; |
|
||||
} |
|
||||
|
|
||||
h1 { |
|
||||
font-size: 18px; |
|
||||
font-weight: 200; |
|
||||
color: $main; |
|
||||
} |
|
||||
} |
|
@ -1,71 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import ReactModal from 'react-modal' |
|
||||
import { FaClose } from 'react-icons/lib/fa' |
|
||||
import styles from './PeerForm.scss' |
|
||||
|
|
||||
const PeerForm = ({ form, setForm, connect }) => { |
|
||||
const submit = () => { |
|
||||
const { pubkey, host } = form |
|
||||
connect({ pubkey, host }) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
<ReactModal |
|
||||
isOpen={form.isOpen} |
|
||||
contentLabel='No Overlay Click Modal' |
|
||||
ariaHideApp |
|
||||
shouldCloseOnOverlayClick |
|
||||
onRequestClose={() => setForm({ isOpen: false })} |
|
||||
parentSelector={() => document.body} |
|
||||
className={styles.modal} |
|
||||
> |
|
||||
<div onClick={() => setForm({ isOpen: false })} className={styles.modalClose}> |
|
||||
<FaClose /> |
|
||||
</div> |
|
||||
|
|
||||
<div className={styles.form} onKeyPress={event => event.charCode === 13 && submit()}> |
|
||||
<h1 className={styles.title}>Connect to a peer</h1> |
|
||||
|
|
||||
<section className={styles.pubkey}> |
|
||||
<label htmlFor='pubkey'>Pubkey</label> |
|
||||
<input |
|
||||
type='text' |
|
||||
size='' |
|
||||
placeholder='Public key' |
|
||||
value={form.pubkey} |
|
||||
onChange={event => setForm({ pubkey: event.target.value })} |
|
||||
id='pubkey' |
|
||||
/> |
|
||||
</section> |
|
||||
<section className={styles.local}> |
|
||||
<label htmlFor='address'>Address</label> |
|
||||
<input |
|
||||
type='text' |
|
||||
size='' |
|
||||
placeholder='Host address' |
|
||||
value={form.host} |
|
||||
onChange={event => setForm({ host: event.target.value })} |
|
||||
id='address' |
|
||||
/> |
|
||||
</section> |
|
||||
|
|
||||
<div className='buttonContainer' onClick={submit}> |
|
||||
<div className='buttonPrimary'> |
|
||||
Submit |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</ReactModal> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
PeerForm.propTypes = { |
|
||||
form: PropTypes.object.isRequired, |
|
||||
setForm: PropTypes.func.isRequired, |
|
||||
connect: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default PeerForm |
|
@ -1,107 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.modal { |
|
||||
position: relative; |
|
||||
width: 40%; |
|
||||
margin: 50px auto; |
|
||||
padding: 40px; |
|
||||
position: absolute; |
|
||||
top: auto; |
|
||||
left: 20%; |
|
||||
right: 0; |
|
||||
bottom: auto; |
|
||||
background: $white; |
|
||||
outline: none; |
|
||||
z-index: -2; |
|
||||
border: 1px solid $darkgrey; |
|
||||
} |
|
||||
|
|
||||
.modalClose { |
|
||||
position: absolute; |
|
||||
top: -13px; |
|
||||
right: -13px; |
|
||||
display: block; |
|
||||
font-size: 16px; |
|
||||
line-height: 27px; |
|
||||
width: 32px; |
|
||||
height: 32px; |
|
||||
background: $white; |
|
||||
border-radius: 50%; |
|
||||
color: $darkestgrey; |
|
||||
cursor: pointer; |
|
||||
text-align: center; |
|
||||
z-index: 2; |
|
||||
transition: all 0.25s; |
|
||||
} |
|
||||
|
|
||||
.modalClose:hover { |
|
||||
background: $darkgrey; |
|
||||
} |
|
||||
|
|
||||
.title { |
|
||||
text-align: center; |
|
||||
font-size: 24px; |
|
||||
color: $black; |
|
||||
margin-bottom: 50px; |
|
||||
} |
|
||||
|
|
||||
.pubkey, .local, .push { |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
font-size: 18px; |
|
||||
height: auto; |
|
||||
min-height: 55px; |
|
||||
margin-bottom: 20px; |
|
||||
border: 1px solid $traditionalgrey; |
|
||||
border-radius: 6px; |
|
||||
position: relative; |
|
||||
padding: 0 20px; |
|
||||
|
|
||||
label, input[type=text] { |
|
||||
font-size: inherit; |
|
||||
} |
|
||||
|
|
||||
label { |
|
||||
padding-top: 19px; |
|
||||
padding-bottom: 12px; |
|
||||
color: $traditionalgrey; |
|
||||
} |
|
||||
|
|
||||
input[type=text] { |
|
||||
width: 100%; |
|
||||
border: none; |
|
||||
outline: 0; |
|
||||
-webkit-appearance: none; |
|
||||
height: 55px; |
|
||||
padding: 0 10px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.buttonGroup { |
|
||||
width: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
border-radius: 6px; |
|
||||
overflow: hidden; |
|
||||
|
|
||||
.button { |
|
||||
cursor: pointer; |
|
||||
height: 55px; |
|
||||
min-height: 55px; |
|
||||
text-transform: none; |
|
||||
font-size: 18px; |
|
||||
transition: opacity .2s ease-out; |
|
||||
background: $main; |
|
||||
color: $white; |
|
||||
border: none; |
|
||||
font-weight: 500; |
|
||||
padding: 0; |
|
||||
width: 100%; |
|
||||
text-align: center; |
|
||||
line-height: 55px; |
|
||||
|
|
||||
&:first-child { |
|
||||
border-right: 1px solid lighten($main, 20%); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,78 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
|
|
||||
import ReactModal from 'react-modal' |
|
||||
import { FaClose } from 'react-icons/lib/fa' |
|
||||
|
|
||||
import styles from './PeerModal.scss' |
|
||||
|
|
||||
const PeerModal = ({ isOpen, resetPeer, peer, disconnect }) => { |
|
||||
const customStyles = { |
|
||||
overlay: { |
|
||||
cursor: 'pointer', |
|
||||
overflowY: 'auto' |
|
||||
}, |
|
||||
content: { |
|
||||
top: 'auto', |
|
||||
left: '20%', |
|
||||
right: '0', |
|
||||
bottom: 'auto', |
|
||||
width: '40%', |
|
||||
margin: '50px auto', |
|
||||
borderRadius: 'none', |
|
||||
padding: '0' |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<ReactModal |
|
||||
isOpen={isOpen} |
|
||||
contentLabel='No Overlay Click Modal' |
|
||||
ariaHideApp |
|
||||
shouldCloseOnOverlayClick |
|
||||
onRequestClose={() => resetPeer(null)} |
|
||||
parentSelector={() => document.body} |
|
||||
style={customStyles} |
|
||||
> |
|
||||
<div className={styles.closeContainer}> |
|
||||
<span onClick={() => resetPeer(null)}> |
|
||||
<FaClose /> |
|
||||
</span> |
|
||||
</div> |
|
||||
{ |
|
||||
peer && |
|
||||
<div className={styles.peer}> |
|
||||
<header className={styles.header}> |
|
||||
<h1 data-hint='Peer address' className='hint--top-left'>{peer.address}</h1> |
|
||||
<h2 data-hint='Peer public key' className='hint--top-left'>{peer.pub_key}</h2> |
|
||||
</header> |
|
||||
|
|
||||
<div className={styles.details}> |
|
||||
<dl> |
|
||||
<dt>Satoshis Received</dt> |
|
||||
<dd>{peer.sat_recv}</dd> |
|
||||
<dt>Satoshis Sent</dt> |
|
||||
<dd>{peer.sat_sent}</dd> |
|
||||
<dt>Bytes Received</dt> |
|
||||
<dd>{peer.bytes_recv}</dd> |
|
||||
<dt>Bytes Sent</dt> |
|
||||
<dd>{peer.bytes_sent}</dd> |
|
||||
</dl> |
|
||||
</div> |
|
||||
<div className={styles.close} onClick={() => disconnect({ pubkey: peer.pub_key })}> |
|
||||
<div>Disconnect peer</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
} |
|
||||
</ReactModal> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
PeerModal.propTypes = { |
|
||||
isOpen: PropTypes.bool.isRequired, |
|
||||
resetPeer: PropTypes.func.isRequired, |
|
||||
peer: PropTypes.object, |
|
||||
disconnect: PropTypes.func.isRequired |
|
||||
} |
|
||||
|
|
||||
export default PeerModal |
|
@ -1,75 +0,0 @@ |
|||||
@import '../../variables.scss'; |
|
||||
|
|
||||
.closeContainer { |
|
||||
background: $lightgrey; |
|
||||
text-align: right; |
|
||||
padding: 10px; |
|
||||
|
|
||||
span { |
|
||||
color: $darkestgrey; |
|
||||
font-size: 20px; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.header { |
|
||||
background: $lightgrey; |
|
||||
padding: 20px; |
|
||||
|
|
||||
h1 { |
|
||||
color: $black; |
|
||||
text-align: center; |
|
||||
margin-bottom: 20px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
h2 { |
|
||||
color: $darkestgrey; |
|
||||
font-size: 12px; |
|
||||
text-align: center; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.details { |
|
||||
dl { |
|
||||
padding: 40px 40px 40px 40px; |
|
||||
} |
|
||||
|
|
||||
dt { |
|
||||
text-align: left; |
|
||||
float: left; |
|
||||
clear: left; |
|
||||
font-weight: 500; |
|
||||
padding: 20px 35px 19px 0; |
|
||||
color: $black; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
dd { |
|
||||
text-align: right; |
|
||||
font-weight: 400; |
|
||||
padding: 30px 0 10px 0; |
|
||||
margin-left: 0; |
|
||||
border-bottom: 1px solid $darkgrey; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.close { |
|
||||
text-align: center; |
|
||||
padding-bottom: 40px; |
|
||||
|
|
||||
div { |
|
||||
margin: 0 auto; |
|
||||
cursor: pointer; |
|
||||
font-size: 18px; |
|
||||
color: $red; |
|
||||
border: none; |
|
||||
padding: 0; |
|
||||
text-align: center; |
|
||||
transition: all 0.25s; |
|
||||
|
|
||||
&:hover { |
|
||||
color: lighten($red, 10%); |
|
||||
} |
|
||||
} |
|
||||
} |
|
After Width: | Height: | Size: 304 B |
@ -1,11 +1,6 @@ |
|||||
export default function subscribeToChannelGraph(mainWindow, lnd, meta) { |
export default function subscribeToChannelGraph(mainWindow, lnd, meta) { |
||||
console.log('subscribeChannelGraph is happening') |
|
||||
|
|
||||
|
|
||||
const call = lnd.subscribeChannelGraph({}, meta) |
const call = lnd.subscribeChannelGraph({}, meta) |
||||
|
|
||||
call.on('data', channelGraphData => mainWindow.send('channelGraphData', { channelGraphData })) |
call.on('data', channelGraphData => mainWindow.send('channelGraphData', { channelGraphData })) |
||||
call.on('end', () => console.log('channel graph end')) |
|
||||
call.on('error', error => console.log('channelgraph error: ', error)) |
|
||||
call.on('status', channelGraphStatus => mainWindow.send('channelGraphStatus', { channelGraphStatus })) |
call.on('status', channelGraphStatus => mainWindow.send('channelGraphStatus', { channelGraphStatus })) |
||||
} |
} |
||||
|
@ -0,0 +1,99 @@ |
|||||
|
import { createSelector } from 'reselect' |
||||
|
|
||||
|
import filter from 'lodash/filter' |
||||
|
|
||||
|
// Initial State
|
||||
|
const initialState = { |
||||
|
isOpen: false, |
||||
|
searchQuery: '', |
||||
|
contactCapacity: 0.1 |
||||
|
} |
||||
|
|
||||
|
// Constants
|
||||
|
// ------------------------------------
|
||||
|
export const OPEN_CONTACTS_FORM = 'OPEN_CONTACTS_FORM' |
||||
|
export const CLOSE_CONTACTS_FORM = 'CLOSE_CONTACTS_FORM' |
||||
|
|
||||
|
export const UPDATE_CONTACT_FORM_SEARCH_QUERY = 'UPDATE_CONTACT_FORM_SEARCH_QUERY' |
||||
|
|
||||
|
export const UPDATE_CONTACT_CAPACITY = 'UPDATE_CONTACT_CAPACITY' |
||||
|
|
||||
|
// ------------------------------------
|
||||
|
// Actions
|
||||
|
// ------------------------------------
|
||||
|
export function openContactsForm() { |
||||
|
return { |
||||
|
type: OPEN_CONTACTS_FORM |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function closeContactsForm() { |
||||
|
return { |
||||
|
type: CLOSE_CONTACTS_FORM |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function updateContactFormSearchQuery(searchQuery) { |
||||
|
return { |
||||
|
type: UPDATE_CONTACT_FORM_SEARCH_QUERY, |
||||
|
searchQuery |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function updateContactCapacity(contactCapacity) { |
||||
|
return { |
||||
|
type: UPDATE_CONTACT_CAPACITY, |
||||
|
contactCapacity |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ------------------------------------
|
||||
|
// Action Handlers
|
||||
|
// ------------------------------------
|
||||
|
const ACTION_HANDLERS = { |
||||
|
[OPEN_CONTACTS_FORM]: state => ({ ...state, isOpen: true }), |
||||
|
[CLOSE_CONTACTS_FORM]: state => ({ ...state, isOpen: false }), |
||||
|
|
||||
|
[UPDATE_CONTACT_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }), |
||||
|
|
||||
|
[UPDATE_CONTACT_CAPACITY]: (state, { contactCapacity }) => ({ ...state, contactCapacity }) |
||||
|
} |
||||
|
|
||||
|
// ------------------------------------
|
||||
|
// Selector
|
||||
|
// ------------------------------------
|
||||
|
const contactFormSelectors = {} |
||||
|
const networkNodesSelector = state => state.network.nodes |
||||
|
const searchQuerySelector = state => state.contactsform.searchQuery |
||||
|
|
||||
|
|
||||
|
contactFormSelectors.filteredNetworkNodes = createSelector( |
||||
|
networkNodesSelector, |
||||
|
searchQuerySelector, |
||||
|
(nodes, searchQuery) => filter(nodes, node => node.alias.includes(searchQuery) || node.pub_key.includes(searchQuery)) |
||||
|
) |
||||
|
|
||||
|
contactFormSelectors.showManualForm = createSelector( |
||||
|
searchQuerySelector, |
||||
|
contactFormSelectors.filteredNetworkNodes, |
||||
|
(searchQuery, filteredNetworkNodes) => { |
||||
|
if (!searchQuery.length) { return false } |
||||
|
|
||||
|
const connectableNodes = filteredNetworkNodes.filter(node => node.addresses.length > 0) |
||||
|
|
||||
|
if (!filteredNetworkNodes.length || !connectableNodes.length) { return true } |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
export { contactFormSelectors } |
||||
|
|
||||
|
// ------------------------------------
|
||||
|
// Reducer
|
||||
|
// ------------------------------------
|
||||
|
export default function contactFormReducer(state = initialState, action) { |
||||
|
const handler = ACTION_HANDLERS[action.type] |
||||
|
|
||||
|
return handler ? handler(state, action) : state |
||||
|
} |
@ -1,104 +0,0 @@ |
|||||
import { withRouter } from 'react-router' |
|
||||
import { connect } from 'react-redux' |
|
||||
|
|
||||
import { |
|
||||
fetchChannels, |
|
||||
openChannel, |
|
||||
closeChannel, |
|
||||
updateChannelSearchQuery, |
|
||||
setViewType, |
|
||||
currentChannels, |
|
||||
|
|
||||
toggleFilterPulldown, |
|
||||
changeFilter, |
|
||||
|
|
||||
channelsSelectors |
|
||||
} from 'reducers/channels' |
|
||||
|
|
||||
import { |
|
||||
openChannelForm, |
|
||||
changeStep, |
|
||||
setNodeKey, |
|
||||
setLocalAmount, |
|
||||
setPushAmount, |
|
||||
closeChannelForm, |
|
||||
channelFormSelectors |
|
||||
} from 'reducers/channelform' |
|
||||
|
|
||||
import { fetchPeers } from 'reducers/peers' |
|
||||
|
|
||||
import { tickerSelectors } from 'reducers/ticker' |
|
||||
|
|
||||
import { fetchDescribeNetwork, setCurrentChannel } from '../../../reducers/network' |
|
||||
|
|
||||
import Channels from '../components/Channels' |
|
||||
|
|
||||
const mapDispatchToProps = { |
|
||||
fetchChannels, |
|
||||
openChannel, |
|
||||
closeChannel, |
|
||||
updateChannelSearchQuery, |
|
||||
setViewType, |
|
||||
toggleFilterPulldown, |
|
||||
changeFilter, |
|
||||
|
|
||||
openChannelForm, |
|
||||
closeChannelForm, |
|
||||
setNodeKey, |
|
||||
setLocalAmount, |
|
||||
setPushAmount, |
|
||||
changeStep, |
|
||||
|
|
||||
fetchPeers, |
|
||||
|
|
||||
fetchDescribeNetwork, |
|
||||
setCurrentChannel |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => ({ |
|
||||
channels: state.channels, |
|
||||
openChannels: state.channels.channels, |
|
||||
channelform: state.channelform, |
|
||||
peers: state.peers, |
|
||||
ticker: state.ticker, |
|
||||
network: state.network, |
|
||||
identity_pubkey: state.info.data.identity_pubkey, |
|
||||
|
|
||||
currentChannels: currentChannels(state), |
|
||||
activeChanIds: channelsSelectors.activeChanIds(state), |
|
||||
nonActiveFilters: channelsSelectors.nonActiveFilters(state), |
|
||||
activeChannels: channelsSelectors.activeChannels(state), |
|
||||
|
|
||||
currentTicker: tickerSelectors.currentTicker(state), |
|
||||
|
|
||||
channelFormHeader: channelFormSelectors.channelFormHeader(state), |
|
||||
channelFormProgress: channelFormSelectors.channelFormProgress(state), |
|
||||
stepTwoIsValid: channelFormSelectors.stepTwoIsValid(state) |
|
||||
}) |
|
||||
|
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
|
||||
const channelFormProps = { |
|
||||
openChannel: dispatchProps.openChannel, |
|
||||
closeChannelForm: dispatchProps.closeChannelForm, |
|
||||
changeStep: dispatchProps.changeStep, |
|
||||
setNodeKey: dispatchProps.setNodeKey, |
|
||||
setLocalAmount: dispatchProps.setLocalAmount, |
|
||||
setPushAmount: dispatchProps.setPushAmount, |
|
||||
|
|
||||
channelform: stateProps.channelform, |
|
||||
channelFormHeader: stateProps.channelFormHeader, |
|
||||
channelFormProgress: stateProps.channelFormProgress, |
|
||||
stepTwoIsValid: stateProps.stepTwoIsValid, |
|
||||
peers: stateProps.peers.peers |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
...stateProps, |
|
||||
...dispatchProps, |
|
||||
...ownProps, |
|
||||
|
|
||||
channelFormProps |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Channels)) |
|
@ -1,3 +0,0 @@ |
|||||
import ChannelsContainer from './containers/ChannelsContainer' |
|
||||
|
|
||||
export default ChannelsContainer |
|
@ -0,0 +1,109 @@ |
|||||
|
import { withRouter } from 'react-router' |
||||
|
import { connect } from 'react-redux' |
||||
|
|
||||
|
import { |
||||
|
fetchChannels, |
||||
|
openChannel, |
||||
|
closeChannel, |
||||
|
|
||||
|
updateChannelSearchQuery, |
||||
|
toggleFilterPulldown, |
||||
|
changeFilter, |
||||
|
openContactModal, |
||||
|
closeContactModal, |
||||
|
currentChannels, |
||||
|
|
||||
|
channelsSelectors |
||||
|
} from 'reducers/channels' |
||||
|
|
||||
|
import { fetchPeers } from 'reducers/peers' |
||||
|
|
||||
|
import { fetchDescribeNetwork } from 'reducers/network' |
||||
|
|
||||
|
import { |
||||
|
openContactsForm, |
||||
|
closeContactsForm, |
||||
|
updateContactFormSearchQuery, |
||||
|
updateContactCapacity, |
||||
|
contactFormSelectors |
||||
|
} from 'reducers/contactsform' |
||||
|
|
||||
|
import Contacts from '../components/Contacts' |
||||
|
|
||||
|
const mapDispatchToProps = { |
||||
|
openContactsForm, |
||||
|
closeContactsForm, |
||||
|
openContactModal, |
||||
|
closeContactModal, |
||||
|
updateContactFormSearchQuery, |
||||
|
updateContactCapacity, |
||||
|
openChannel, |
||||
|
closeChannel, |
||||
|
updateChannelSearchQuery, |
||||
|
toggleFilterPulldown, |
||||
|
changeFilter, |
||||
|
|
||||
|
fetchChannels, |
||||
|
fetchPeers, |
||||
|
fetchDescribeNetwork |
||||
|
} |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
channels: state.channels, |
||||
|
peers: state.peers, |
||||
|
network: state.network, |
||||
|
contactsform: state.contactsform, |
||||
|
|
||||
|
currentChannels: currentChannels(state), |
||||
|
activeChannels: channelsSelectors.activeChannels(state), |
||||
|
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state), |
||||
|
nonActiveChannels: channelsSelectors.nonActiveChannels(state), |
||||
|
nonActiveChannelPubkeys: channelsSelectors.nonActiveChannelPubkeys(state), |
||||
|
pendingOpenChannels: channelsSelectors.pendingOpenChannels(state), |
||||
|
pendingOpenChannelPubkeys: channelsSelectors.pendingOpenChannelPubkeys(state), |
||||
|
closingPendingChannels: channelsSelectors.closingPendingChannels(state), |
||||
|
nonActiveFilters: channelsSelectors.nonActiveFilters(state), |
||||
|
channelNodes: channelsSelectors.channelNodes(state), |
||||
|
|
||||
|
filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state), |
||||
|
showManualForm: contactFormSelectors.showManualForm(state) |
||||
|
}) |
||||
|
|
||||
|
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
||||
|
const contactModalProps = { |
||||
|
closeContactModal: dispatchProps.closeContactModal, |
||||
|
closeChannel: dispatchProps.closeChannel, |
||||
|
|
||||
|
isOpen: stateProps.channels.contactModal.isOpen, |
||||
|
channel: stateProps.channels.contactModal.channel, |
||||
|
channelNodes: stateProps.channelNodes, |
||||
|
closingChannelIds: stateProps.channels.closingChannelIds |
||||
|
} |
||||
|
|
||||
|
const contactsFormProps = { |
||||
|
closeContactsForm: dispatchProps.closeContactsForm, |
||||
|
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery, |
||||
|
updateContactCapacity: dispatchProps.updateContactCapacity, |
||||
|
openChannel: dispatchProps.openChannel, |
||||
|
|
||||
|
contactsform: stateProps.contactsform, |
||||
|
filteredNetworkNodes: stateProps.filteredNetworkNodes, |
||||
|
loadingChannelPubkeys: stateProps.channels.loadingChannelPubkeys, |
||||
|
showManualForm: stateProps.showManualForm, |
||||
|
|
||||
|
activeChannelPubkeys: stateProps.activeChannelPubkeys, |
||||
|
nonActiveChannelPubkeys: stateProps.nonActiveChannelPubkeys, |
||||
|
pendingOpenChannelPubkeys: stateProps.pendingOpenChannelPubkeys |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
...stateProps, |
||||
|
...dispatchProps, |
||||
|
...ownProps, |
||||
|
|
||||
|
contactModalProps, |
||||
|
contactsFormProps |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Contacts)) |
@ -0,0 +1,3 @@ |
|||||
|
import ContactsContainer from './containers/ContactsContainer' |
||||
|
|
||||
|
export default ContactsContainer |
@ -1,137 +0,0 @@ |
|||||
import React, { Component } from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
|
|
||||
import { FaRepeat } from 'react-icons/lib/fa' |
|
||||
import { MdSearch } from 'react-icons/lib/md' |
|
||||
|
|
||||
import PeerForm from 'components/Peers/PeerForm' |
|
||||
import PeerModal from 'components/Peers/PeerModal' |
|
||||
import Peer from 'components/Peers/Peer' |
|
||||
|
|
||||
import styles from './Peers.scss' |
|
||||
|
|
||||
class Peers extends Component { |
|
||||
constructor(props) { |
|
||||
super(props) |
|
||||
|
|
||||
this.state = { |
|
||||
refreshing: false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentWillMount() { |
|
||||
this.props.fetchPeers() |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { |
|
||||
fetchPeers, |
|
||||
peerFormProps, |
|
||||
setPeerForm, |
|
||||
setPeer, |
|
||||
updateSearchQuery, |
|
||||
disconnectRequest, |
|
||||
|
|
||||
peerModalOpen, |
|
||||
filteredPeers, |
|
||||
peers: { peer, searchQuery } |
|
||||
} = this.props |
|
||||
|
|
||||
const refreshClicked = () => { |
|
||||
// turn the spinner on
|
|
||||
this.setState({ refreshing: true }) |
|
||||
|
|
||||
// store event in icon so we dont get an error when react clears it
|
|
||||
const icon = this.repeat.childNodes |
|
||||
|
|
||||
// fetch peers
|
|
||||
fetchPeers() |
|
||||
|
|
||||
// wait for the svg to appear as child
|
|
||||
const svgTimeout = setTimeout(() => { |
|
||||
if (icon[0].tagName === 'svg') { |
|
||||
// spin icon for 1 sec
|
|
||||
icon[0].style.animation = 'spin 1000ms linear 1' |
|
||||
clearTimeout(svgTimeout) |
|
||||
} |
|
||||
}, 1) |
|
||||
|
|
||||
// clear animation after the second so we can reuse it
|
|
||||
const refreshTimeout = setTimeout(() => { |
|
||||
icon[0].style.animation = '' |
|
||||
this.setState({ refreshing: false }) |
|
||||
clearTimeout(refreshTimeout) |
|
||||
}, 1000) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
<PeerForm {...peerFormProps} /> |
|
||||
|
|
||||
<PeerModal isOpen={peerModalOpen} resetPeer={setPeer} peer={peer} disconnect={disconnectRequest} /> |
|
||||
|
|
||||
<header className={styles.header}> |
|
||||
<div className={styles.titleContainer}> |
|
||||
<div className={styles.left}> |
|
||||
<h1>Peers</h1> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className={styles.addPeerContainer}> |
|
||||
<div className={`buttonPrimary ${styles.newPeerButton}`} onClick={() => setPeerForm({ isOpen: true })}> |
|
||||
Add new peer |
|
||||
</div> |
|
||||
</div> |
|
||||
</header> |
|
||||
|
|
||||
<div className={styles.search}> |
|
||||
<label className={`${styles.label} ${styles.input}`} htmlFor='channelSearch'> |
|
||||
<MdSearch /> |
|
||||
</label> |
|
||||
<input |
|
||||
value={searchQuery} |
|
||||
onChange={event => updateSearchQuery(event.target.value)} |
|
||||
className={`${styles.text} ${styles.input}`} |
|
||||
placeholder='Search peers by their node public key or IP address' |
|
||||
type='text' |
|
||||
id='peersSearch' |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<div className={styles.refreshContainer}> |
|
||||
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}> |
|
||||
{ |
|
||||
this.state.refreshing ? |
|
||||
<FaRepeat /> |
|
||||
: |
|
||||
'Refresh' |
|
||||
} |
|
||||
</span> |
|
||||
</div> |
|
||||
|
|
||||
<div className={styles.peers}> |
|
||||
{ |
|
||||
filteredPeers.map(filteredPeer => <Peer key={filteredPeer.peer_id} peer={filteredPeer} setPeer={setPeer} />) |
|
||||
} |
|
||||
</div> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Peers.propTypes = { |
|
||||
fetchPeers: PropTypes.func.isRequired, |
|
||||
peerFormProps: PropTypes.object.isRequired, |
|
||||
setPeerForm: PropTypes.func.isRequired, |
|
||||
setPeer: PropTypes.func.isRequired, |
|
||||
updateSearchQuery: PropTypes.func.isRequired, |
|
||||
disconnectRequest: PropTypes.func.isRequired, |
|
||||
|
|
||||
peerModalOpen: PropTypes.bool.isRequired, |
|
||||
filteredPeers: PropTypes.array.isRequired, |
|
||||
peers: PropTypes.shape({ |
|
||||
peer: PropTypes.object, |
|
||||
searchQuery: PropTypes.string |
|
||||
}).isRequired |
|
||||
} |
|
||||
|
|
||||
export default Peers |
|
@ -1,81 +0,0 @@ |
|||||
@import '../../../variables.scss'; |
|
||||
|
|
||||
.search { |
|
||||
height: 55px; |
|
||||
padding: 2px 25px; |
|
||||
border-top: 1px solid $darkgrey; |
|
||||
border-bottom: 1px solid $darkgrey; |
|
||||
background: $white; |
|
||||
|
|
||||
.input { |
|
||||
display: inline-block; |
|
||||
vertical-align: top; |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
.label { |
|
||||
width: 5%; |
|
||||
line-height: 50px; |
|
||||
font-size: 20px; |
|
||||
text-align: center; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.text { |
|
||||
width: 95%; |
|
||||
outline: 0; |
|
||||
padding: 0; |
|
||||
border: 0; |
|
||||
border-radius: 0; |
|
||||
height: 50px; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.header { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
background: $lightgrey; |
|
||||
|
|
||||
.titleContainer { |
|
||||
padding: 20px 40px; |
|
||||
|
|
||||
.left { |
|
||||
padding: 10px 0; |
|
||||
|
|
||||
h1 { |
|
||||
text-transform: uppercase; |
|
||||
font-size: 26px; |
|
||||
margin-right: 5px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.addPeerContainer { |
|
||||
padding: 20px 40px; |
|
||||
|
|
||||
.newPeerButton { |
|
||||
font-size: 14px; |
|
||||
margin-left: 10px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.refreshContainer { |
|
||||
padding: 20px 40px 0 40px; |
|
||||
text-align: right; |
|
||||
cursor: pointer; |
|
||||
|
|
||||
.refresh { |
|
||||
text-decoration: underline; |
|
||||
|
|
||||
svg { |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.peers { |
|
||||
padding: 40px; |
|
||||
} |
|
@ -1,52 +0,0 @@ |
|||||
import { withRouter } from 'react-router' |
|
||||
import { connect } from 'react-redux' |
|
||||
|
|
||||
import { |
|
||||
fetchPeers, |
|
||||
setPeer, |
|
||||
setPeerForm, |
|
||||
connectRequest, |
|
||||
disconnectRequest, |
|
||||
updateSearchQuery, |
|
||||
|
|
||||
peersSelectors |
|
||||
} from 'reducers/peers' |
|
||||
|
|
||||
import Peers from '../components/Peers' |
|
||||
|
|
||||
const mapDispatchToProps = { |
|
||||
fetchPeers, |
|
||||
setPeer, |
|
||||
peersSelectors, |
|
||||
setPeerForm, |
|
||||
connectRequest, |
|
||||
disconnectRequest, |
|
||||
updateSearchQuery |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => ({ |
|
||||
peers: state.peers, |
|
||||
info: state.info, |
|
||||
|
|
||||
peerModalOpen: peersSelectors.peerModalOpen(state), |
|
||||
filteredPeers: peersSelectors.filteredPeers(state) |
|
||||
}) |
|
||||
|
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
|
||||
const peerFormProps = { |
|
||||
setForm: dispatchProps.setPeerForm, |
|
||||
connect: dispatchProps.connectRequest, |
|
||||
|
|
||||
form: stateProps.peers.peerForm |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
...stateProps, |
|
||||
...dispatchProps, |
|
||||
...ownProps, |
|
||||
|
|
||||
peerFormProps |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Peers)) |
|
@ -1,3 +0,0 @@ |
|||||
import PeersContainer from './containers/PeersContainer' |
|
||||
|
|
||||
export default PeersContainer |
|
@ -1,125 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import { shallow } from 'enzyme' |
|
||||
|
|
||||
import { TiPlus } from 'react-icons/lib/ti' |
|
||||
import Channels from '../../app/components/Channels' |
|
||||
import ChannelModal from '../../app/components/Channels/ChannelModal' |
|
||||
import ChannelForm from '../../app/components/Channels/ChannelForm' |
|
||||
import Channel from '../../app/components/Channels/Channel' |
|
||||
import OpenPendingChannel from '../../app/components/Channels/OpenPendingChannel' |
|
||||
import ClosedPendingChannel from '../../app/components/Channels/ClosedPendingChannel' |
|
||||
|
|
||||
const defaultProps = { |
|
||||
ticker: {}, |
|
||||
peers: [], |
|
||||
channelsLoading: false, |
|
||||
modalChannel: {}, |
|
||||
setChannel: () => {}, |
|
||||
channelModalOpen: false, |
|
||||
channelForm: {}, |
|
||||
setChannelForm: () => {}, |
|
||||
allChannels: [], |
|
||||
openChannel: () => {}, |
|
||||
closeChannel: () => {}, |
|
||||
fetchChannels: () => {}, |
|
||||
currentTicker: {}, |
|
||||
explorerLinkBase: 'https://testnet.smartbit.com.au' |
|
||||
} |
|
||||
|
|
||||
const channel_open = { |
|
||||
active: true, |
|
||||
capacity: '10000000', |
|
||||
chan_id: '1322138543153545216', |
|
||||
channel_point: '7efb80bf568cf55eb43ba439fdafea99b43f53493ec9ae7c0eae88de2d2b4577:0', |
|
||||
commit_fee: '8688', |
|
||||
commit_weight: '600', |
|
||||
fee_per_kw: '12000', |
|
||||
local_balance: '9991312', |
|
||||
num_updates: '0', |
|
||||
pending_htlcs: [], |
|
||||
remote_balance: '0', |
|
||||
remote_pubkey: '020178567c0f881b579a7ddbcd8ce362a33ebba2b3c2d218e667f7e3b390e40d4e', |
|
||||
total_satoshis_received: '0', |
|
||||
total_satoshis_sent: '0', |
|
||||
unsettled_balance: '0' |
|
||||
} |
|
||||
|
|
||||
const channel_pending = { |
|
||||
capacity: '10000000', |
|
||||
channel_point: '7efb80bf568cf55eb43ba439fdafea99b43f53493ec9ae7c0eae88de2d2b4577:0', |
|
||||
local_balance: '9991312', |
|
||||
remote_balance: '0', |
|
||||
remote_node_pub: '020178567c0f881b579a7ddbcd8ce362a33ebba2b3c2d218e667f7e3b390e40d4e' |
|
||||
} |
|
||||
|
|
||||
const pending_open_channels = { |
|
||||
blocks_till_open: 0, |
|
||||
channel: channel_pending, |
|
||||
commit_fee: '8688', |
|
||||
commit_weight: '600', |
|
||||
confirmation_height: 0, |
|
||||
fee_per_kw: '12000' |
|
||||
} |
|
||||
|
|
||||
const pending_closing_channels = { |
|
||||
channel: channel_pending, |
|
||||
closing_txid: '8d623d1ddd32945cace3351d511df2b5be3e0f7c7e5622989d2fc0215e8a2a7e' |
|
||||
} |
|
||||
|
|
||||
describe('Channels', () => { |
|
||||
describe('should show default components', () => { |
|
||||
const props = { ...defaultProps, channelsLoading: true } |
|
||||
const el = shallow(<Channels {...props} />) |
|
||||
it('should contain Modal and Form', () => { |
|
||||
expect(el.find(ChannelModal)).toHaveLength(1) |
|
||||
expect(el.find(ChannelForm)).toHaveLength(1) |
|
||||
}) |
|
||||
it('should have Channels header, and plus button', () => { |
|
||||
expect(el.contains('Channels')).toBe(true) |
|
||||
expect(el.find(TiPlus)).toHaveLength(1) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('channels are loading', () => { |
|
||||
const props = { ...defaultProps, channelsLoading: true } |
|
||||
const el = shallow(<Channels {...props} />) |
|
||||
it('should display loading msg', () => { |
|
||||
expect(el.contains('Loading...')).toBe(true) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('channels are loaded', () => { |
|
||||
describe('no channels', () => { |
|
||||
const props = { ...defaultProps, allChannels: [] } |
|
||||
const el = shallow(<Channels {...props} />) |
|
||||
it('should not show channels or loading', () => { |
|
||||
expect(el.contains('Loading...')).toBe(false) |
|
||||
expect(el.find(Channel)).toHaveLength(0) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('channel is open-pending', () => { |
|
||||
const props = { ...defaultProps, allChannels: [pending_open_channels] } |
|
||||
const el = shallow(<Channels {...props} />) |
|
||||
it('should display open-pending', () => { |
|
||||
expect(el.find(OpenPendingChannel)).toHaveLength(1) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('channel is open', () => { |
|
||||
const props = { ...defaultProps, allChannels: [channel_open] } |
|
||||
const el = shallow(<Channels {...props} />) |
|
||||
it('should display open channel', () => { |
|
||||
expect(el.find(Channel)).toHaveLength(1) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('channel is closed-pending', () => { |
|
||||
const props = { ...defaultProps, allChannels: [pending_closing_channels] } |
|
||||
const el = shallow(<Channels {...props} />) |
|
||||
it('should display closed-pending', () => { |
|
||||
expect(el.find(ClosedPendingChannel)).toHaveLength(1) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -1,72 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import { shallow } from 'enzyme' |
|
||||
|
|
||||
import Peers from '../../app/routes/peers/components/Peers' |
|
||||
import PeerModal from '../../app/components/Peers/PeerModal' |
|
||||
import PeerForm from '../../app/components/Peers/PeerForm' |
|
||||
import Peer from '../../app/components/Peers/Peer' |
|
||||
|
|
||||
const defaultProps = { |
|
||||
fetchPeers: () => {}, |
|
||||
peerFormProps: { |
|
||||
form: {}, |
|
||||
setForm: () => {}, |
|
||||
connect: () => {} |
|
||||
}, |
|
||||
setPeerForm: () => {}, |
|
||||
setPeer: () => {}, |
|
||||
updateSearchQuery: () => {}, |
|
||||
disconnectRequest: () => {}, |
|
||||
|
|
||||
peerModalOpen: false, |
|
||||
filteredPeers: [], |
|
||||
peers: { |
|
||||
peer: null, |
|
||||
searchQuery: '' |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const peer = { |
|
||||
address: '45.77.115.33:9735', |
|
||||
bytes_recv: '63322', |
|
||||
bytes_sent: '68714', |
|
||||
inbound: true, |
|
||||
peer_id: 3, |
|
||||
ping_time: '261996', |
|
||||
pub_key: '0293cb97aac77eacjc5377d761640f1b51ebba350902801e1aa62853fa7bc3a1f30', |
|
||||
sat_recv: '0', |
|
||||
sat_sent: '0' |
|
||||
} |
|
||||
|
|
||||
describe('component.Peers', () => { |
|
||||
describe('default components', () => { |
|
||||
const props = { ...defaultProps } |
|
||||
const el = shallow(<Peers {...props} />) |
|
||||
it('should contain Modal and Form', () => { |
|
||||
expect(el.find(PeerModal)).toHaveLength(1) |
|
||||
expect(el.find(PeerForm)).toHaveLength(1) |
|
||||
}) |
|
||||
it('should have Peers header, and plus button', () => { |
|
||||
expect(el.contains('Peers')).toBe(true) |
|
||||
expect(el.contains('Add new peer')).toBe(true) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('peers are loaded', () => { |
|
||||
describe('no peers', () => { |
|
||||
const props = { ...defaultProps } |
|
||||
const el = shallow(<Peers {...props} />) |
|
||||
it('should show no peers', () => { |
|
||||
expect(el.find(Peer)).toHaveLength(0) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('peer connected', () => { |
|
||||
const props = { ...defaultProps, filteredPeers: [peer] } |
|
||||
const el = shallow(<Peers {...props} />) |
|
||||
it('should show peer information', () => { |
|
||||
expect(el.find(Peer)).toHaveLength(1) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
Loading…
Reference in new issue