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) { |
|||
console.log('subscribeChannelGraph is happening') |
|||
|
|||
|
|||
const call = lnd.subscribeChannelGraph({}, meta) |
|||
|
|||
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 })) |
|||
} |
|||
|
@ -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