Browse Source

feature(contacts): start contacts feature

renovate/lint-staged-8.x
Jack Mallers 7 years ago
parent
commit
37be5c0e1e
  1. 38
      app/components/Friends/ClosingContact.js
  2. 102
      app/components/Friends/Contact.scss
  3. 31
      app/components/Friends/Donut.js
  4. 20
      app/components/Friends/Donut.scss
  5. 146
      app/components/Friends/FriendsForm.js
  6. 135
      app/components/Friends/FriendsForm.scss
  7. 33
      app/components/Friends/OfflineContact.js
  8. 33
      app/components/Friends/OnlineContact.js
  9. 0
      app/components/Friends/OnlineContact.scss
  10. 38
      app/components/Friends/PendingContact.js
  11. 53
      app/lnd/methods/channelController.js
  12. 13
      app/lnd/methods/index.js
  13. 2
      app/lnd/methods/peersController.js
  14. 57
      app/reducers/channels.js
  15. 74
      app/reducers/friendsform.js
  16. 3
      app/reducers/index.js
  17. 100
      app/routes/friends/components/Friends.js
  18. 46
      app/routes/friends/components/Friends.scss
  19. 60
      app/routes/friends/containers/FriendsContainer.js
  20. 1
      app/variables.scss

38
app/components/Friends/ClosingContact.js

@ -0,0 +1,38 @@
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} key={index}>
<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 = {
}
export default ClosingContact

102
app/components/Friends/Contact.scss

@ -0,0 +1,102 @@
@import '../../variables.scss';
.friend {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 30px 0;
border-bottom: 1px solid $traditionalgrey;
.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;
}
}
}
}

31
app/components/Friends/Donut.js

@ -0,0 +1,31 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import styles from './Donut.scss'
const Donut = ({ value, size, strokewidth }) => {
console.log('value: ', value)
console.log('size: ', size)
console.log('strokewidth: ', strokewidth)
const halfsize = (size * 0.5);
const radius = halfsize - (strokewidth * 0.5);
const circumference = 2 * Math.PI * radius;
const strokeval = ((value * circumference) / 100);
const dashval = (strokeval + ' ' + circumference);
const trackstyle = {strokeWidth: 5};
const indicatorstyle = {strokeWidth: strokewidth, strokeDasharray: dashval}
const rotateval = 'rotate(-90 '+37.5+','+37.5+')';
return (
<svg width={75} height={75} className={styles.donutchart}>
<circle r={30} cx={37.5} cy={37.5} transform={rotateval} style={trackstyle} className={styles.donutchartTrack} />
<circle r={30} cx={37.5} cy={37.5} transform={rotateval} style={indicatorstyle} className={styles.donutchartIndicator} />
</svg>
)
}
Donut.propTypes = {
}
export default Donut

20
app/components/Friends/Donut.scss

@ -0,0 +1,20 @@
@import '../../variables.scss';
.donutchartTrack{
fill: transparent;
stroke: $lightgrey;
stroke-width: 26;
}
.donutchartIndicator {
fill: transparent;
stroke: $main;
stroke-width: 26;
stroke-dasharray: 0 10000;
transition: stroke-dasharray .3s ease;
}
.donutchart {
margin: 0 auto;
border-radius: 50%;
display: block;
}

146
app/components/Friends/FriendsForm.js

@ -0,0 +1,146 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { MdClose } from 'react-icons/lib/md'
import { FaCircle } from 'react-icons/lib/fa'
import styles from './FriendsForm.scss'
const FriendsForm = ({
friendsform,
closeFriendsForm,
updateFriendFormSearchQuery,
openChannel,
activeChannelPubkeys,
nonActiveChannelPubkeys,
pendingOpenChannelPubkeys,
filteredNetworkNodes
}) => {
console.log('pendingOpenChannelPubkeys: ', pendingOpenChannelPubkeys)
const renderRightSide = (node) => {
if (node.addresses.length > 1) {
console.log('node: ', node)
}
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 0.1 BTC'
onClick={() => openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: 0.1 })}
>
Connect
</span>
)
}
return (
<div>
<ReactModal
isOpen={friendsform.isOpen}
contentLabel='No Overlay Click Modal'
ariaHideApp
shouldCloseOnOverlayClick
onRequestClose={() => closeFriendsForm}
parentSelector={() => document.body}
className={styles.modal}
>
<header>
<div>
<h1>Add Contact</h1>
</div>
<div onClick={closeFriendsForm} className={styles.modalClose}>
<MdClose />
</div>
</header>
<div className={styles.form} onKeyPress={event => event.charCode === 13 && console.log('gaaaang')}>
<div className={styles.search}>
<input
type='text'
placeholder='Find friend by alias or pubkey'
className={styles.searchInput}
value={friendsform.searchQuery}
onChange={event => updateFriendFormSearchQuery(event.target.value)}
autoFocus
/>
</div>
<ul className={styles.networkResults}>
{
friendsform.searchQuery.length > 0 && filteredNetworkNodes.map(node => {
return (
<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>
<footer className={styles.footer}>
<div>
<span className={styles.amount}>
0.1
</span>
<span className={styles.caption}>
BTC per contact
</span>
</div>
</footer>
</ReactModal>
</div>
)
}
FriendsForm.propTypes = {
}
export default FriendsForm

135
app/components/Friends/FriendsForm.scss

@ -0,0 +1,135 @@
@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: 400px;
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: 16px;
font-weight: bold;
letter-spacing: 1.3px;
}
&:nth-child(2) {
color: $darkestgrey;
font-size: 12px;
line-height: 16px;
}
}
}
.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%);
}
}
}
}
}
.footer {
padding: 10px 15px;
border-top: 1px solid $darkgrey;
span {
&:nth-child(1) {
font-weight: bold;
}
&:nth-child(2) {
margin-left: 2px;
}
}
}

33
app/components/Friends/OfflineContact.js

@ -0,0 +1,33 @@
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 }) => (
<li className={styles.friend} key={channel.chan_id}>
<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 = {
}
export default OfflineContact

33
app/components/Friends/OnlineContact.js

@ -0,0 +1,33 @@
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 }) => (
<li className={styles.friend} key={channel.chan_id}>
<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 = {
}
export default OnlineContact

0
app/components/Friends/OnlineContact.scss

38
app/components/Friends/PendingContact.js

@ -0,0 +1,38 @@
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]}`)}>
(~{channel.blocks_till_open * 10} minutes)
</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 = {
}
export default PendingContact

53
app/lnd/methods/channelController.js

@ -1,9 +1,61 @@
import bitcore from 'bitcore-lib' import bitcore from 'bitcore-lib'
import find from 'lodash/find'
import { listPeers, connectPeer } from './peersController'
import pushopenchannel from '../push/openchannel' import pushopenchannel from '../push/openchannel'
import pushclosechannel from '../push/closechannel' import pushclosechannel from '../push/closechannel'
const BufferUtil = bitcore.util.buffer const BufferUtil = bitcore.util.buffer
/**
* Attempts to open a singly funded channel specified in the request to a remote peer.
* @param {[type]} lnd [description]
* @param {[type]} event [description]
* @param {[type]} payload [description]
* @return {[type]} [description]
*/
export function connectAndOpen(lnd, meta, event, payload) {
console.log('payload: ', payload)
const { pubkey, host, localamt } = payload
const channelPayload = {
node_pubkey: BufferUtil.hexToBuffer(pubkey),
local_funding_amount: Number(localamt)
}
return new Promise((resolve, reject) => {
listPeers(lnd, meta)
.then(({ peers }) => {
console.log('peers: ', peers)
const peer = find(peers, { pub_key: pubkey })
if (peer) {
console.log('we can open the channel')
} else {
console.log('connect to the peer first')
connectPeer(lnd, meta, { pubkey, host })
.then((data) => {
console.log('connectPeer data: ', data)
const call = lnd.openChannel(channelPayload, meta)
call.on('data', data => event.sender.send('pushchannelupdated', { data }))
call.on('error', error => event.sender.send('pushchannelerror', { error: error.toString() }))
call.on('end', () => event.sender.send('pushchannelend'))
call.on('status', status => event.sender.send('pushchannelstatus', { status }))
})
.catch(err => {
console.log('connectPeer err: ', err)
})
}
})
.catch(err => {
console.log('listPeers err', err)
})
})
}
/** /**
* Attempts to open a singly funded channel specified in the request to a remote peer. * Attempts to open a singly funded channel specified in the request to a remote peer.
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -12,6 +64,7 @@ const BufferUtil = bitcore.util.buffer
* @return {[type]} [description] * @return {[type]} [description]
*/ */
export function openChannel(lnd, meta, event, payload) { export function openChannel(lnd, meta, event, payload) {
console.log('opening the channel')
const { pubkey, localamt, pushamt } = payload const { pubkey, localamt, pushamt } = payload
const res = { const res = {
node_pubkey: BufferUtil.hexToBuffer(pubkey), node_pubkey: BufferUtil.hexToBuffer(pubkey),

13
app/lnd/methods/index.js

@ -201,6 +201,19 @@ export default function (lnd, meta, event, msg, data) {
}) })
.catch(error => console.log('disconnectPeer error: ', error)) .catch(error => console.log('disconnectPeer error: ', error))
break break
case 'connectAndOpen':
// Connects to a peer if we aren't connected already and then attempt to open a channel
// {} = data
channelController.connectAndOpen(lnd, meta, event, data)
.then((data) => {
console.log('connectAndOpen data: ', data)
// event.sender.send('connectSuccess', { pub_key: data.pubkey, address: data.host, peer_id })
})
.catch((error) => {
// event.sender.send('connectFailure', { error: error.toString() })
console.log('connectAndOpen error: ', error)
})
break
default: default:
} }
} }

2
app/lnd/methods/peersController.js

@ -7,7 +7,7 @@
*/ */
export function connectPeer(lnd, meta, { pubkey, host }) { export function connectPeer(lnd, meta, { pubkey, host }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.connectPeer({ addr: { pubkey, host }, perm: true }, meta, (err, data) => { lnd.connectPeer({ addr: { pubkey, host } }, meta, (err, data) => {
if (err) { reject(err) } if (err) { reject(err) }
resolve(data) resolve(data)

57
app/reducers/channels.js

@ -101,12 +101,11 @@ export const fetchChannels = () => async (dispatch) => {
export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels }) export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels })
// Send IPC event for opening a channel // Send IPC event for opening a channel
export const openChannel = ({ pubkey, local_amt, push_amt }) => (dispatch) => { export const openChannel = ({ pubkey, host, local_amt, push_amt }) => (dispatch) => {
const localamt = btc.btcToSatoshis(local_amt) const localamt = btc.btcToSatoshis(local_amt)
const pushamt = btc.btcToSatoshis(push_amt)
dispatch(openingChannel()) dispatch(openingChannel())
ipcRenderer.send('lnd', { msg: 'openChannel', data: { pubkey, localamt, pushamt } }) ipcRenderer.send('lnd', { msg: 'connectAndOpen', data: { pubkey, host, localamt } })
} }
// TODO: Decide how to handle streamed updates for channels // TODO: Decide how to handle streamed updates for channels
@ -293,56 +292,74 @@ channelsSelectors.activeChannels = createSelector(
openChannels => openChannels.filter(channel => channel.active) openChannels => openChannels.filter(channel => channel.active)
) )
channelsSelectors.activeChannelPubkeys = createSelector(
channelsSelector,
openChannels => openChannels.filter(channel => channel.active).map(c => c.remote_pubkey)
)
channelsSelectors.nonActiveChannels = createSelector( channelsSelectors.nonActiveChannels = createSelector(
channelsSelector, channelsSelector,
openChannels => openChannels.filter(channel => !channel.active) openChannels => openChannels.filter(channel => !channel.active)
) )
channelsSelectors.nonActiveChannelPubkeys = createSelector(
channelsSelector,
openChannels => openChannels.filter(channel => !channel.active).map(c => c.remote_pubkey)
)
channelsSelectors.pendingOpenChannels = createSelector( channelsSelectors.pendingOpenChannels = createSelector(
pendingOpenChannelsSelector, pendingOpenChannelsSelector,
pendingOpenChannels => pendingOpenChannels pendingOpenChannels => pendingOpenChannels
) )
const closingPendingChannels = createSelector( channelsSelectors.pendingOpenChannelPubkeys = createSelector(
pendingOpenChannelsSelector,
pendingOpenChannels => pendingOpenChannels.map(pendingChannel => pendingChannel.channel.remote_node_pub)
)
channelsSelectors.closingPendingChannels = createSelector(
pendingClosedChannelsSelector, pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector, pendingForceClosedChannelsSelector,
(pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels] (pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels]
) )
const allChannels = createSelector( channelsSelectors.activeChanIds = createSelector(
channelsSelector, channelsSelector,
channels => channels.map(channel => channel.chan_id)
)
channelsSelectors.nonActiveFilters = createSelector(
filtersSelector,
filterSelector,
(filters, filter) => filters.filter(f => f.key !== filter.key)
)
const allChannels = createSelector(
channelsSelectors.activeChannels,
channelsSelectors.nonActiveChannels,
pendingOpenChannelsSelector, pendingOpenChannelsSelector,
pendingClosedChannelsSelector, pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector, pendingForceClosedChannelsSelector,
channelSearchQuerySelector, channelSearchQuerySelector,
(channels, pendingOpenChannels, pendingClosedChannels, pendingForcedClosedChannels, searchQuery) => { (activeChannels, nonActiveChannels, pendingOpenChannels, pendingClosedChannels, pendingForcedClosedChannels, searchQuery) => {
const filteredChannels = channels.filter(channel => channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)) // eslint-disable-line const filteredActiveChannels = activeChannels.filter(channel => channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredNonActiveChannels = nonActiveChannels.filter(channel => channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingOpenChannels = pendingOpenChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line const filteredPendingOpenChannels = pendingOpenChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingClosedChannels = pendingClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line const filteredPendingClosedChannels = pendingClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingForcedClosedChannels = pendingForcedClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line const filteredPendingForcedClosedChannels = pendingForcedClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
return [...filteredChannels, ...filteredPendingOpenChannels, ...filteredPendingClosedChannels, ...filteredPendingForcedClosedChannels] return [...filteredActiveChannels, ...filteredNonActiveChannels, ...filteredPendingOpenChannels, ...filteredPendingClosedChannels, ...filteredPendingForcedClosedChannels]
} }
) )
channelsSelectors.activeChanIds = createSelector(
channelsSelector,
channels => channels.map(channel => channel.chan_id)
)
channelsSelectors.nonActiveFilters = createSelector(
filtersSelector,
filterSelector,
(filters, filter) => filters.filter(f => f.key !== filter.key)
)
export const currentChannels = createSelector( export const currentChannels = createSelector(
allChannels, allChannels,
channelsSelectors.activeChannels, channelsSelectors.activeChannels,
channelsSelector, channelsSelector,
pendingOpenChannelsSelector, pendingOpenChannelsSelector,
closingPendingChannels, channelsSelectors.closingPendingChannels,
filterSelector, filterSelector,
channelSearchQuerySelector, channelSearchQuerySelector,
(allChannelsArr, activeChannelsArr, openChannels, pendingOpenChannels, pendingClosedChannels, filter, searchQuery) => { (allChannelsArr, activeChannelsArr, openChannels, pendingOpenChannels, pendingClosedChannels, filter, searchQuery) => {

74
app/reducers/friendsform.js

@ -0,0 +1,74 @@
import { createSelector } from 'reselect'
import filter from 'lodash/filter'
// Initial State
const initialState = {
isOpen: false,
searchQuery: '',
friend: ''
}
// Constants
// ------------------------------------
export const OPEN_FRIENDS_FORM = 'OPEN_FRIENDS_FORM'
export const CLOSE_FRIENDS_FORM = 'CLOSE_FRIENDS_FORM'
export const UPDATE_FRIEND_FORM_SEARCH_QUERY = 'UPDATE_FRIEND_FORM_SEARCH_QUERY'
// ------------------------------------
// Actions
// ------------------------------------
export function openFriendsForm() {
return {
type: OPEN_FRIENDS_FORM
}
}
export function closeFriendsForm() {
return {
type: CLOSE_FRIENDS_FORM
}
}
export function updateFriendFormSearchQuery(searchQuery) {
return {
type: UPDATE_FRIEND_FORM_SEARCH_QUERY,
searchQuery
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[OPEN_FRIENDS_FORM]: state => ({ ...state, isOpen: true }),
[CLOSE_FRIENDS_FORM]: state => ({ ...state, isOpen: false }),
[UPDATE_FRIEND_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery })
}
// ------------------------------------
// Selector
// ------------------------------------
const friendFormSelectors = {}
const networkNodesSelector = state => state.network.nodes
const searchQuerySelector = state => state.friendsform.searchQuery
friendFormSelectors.filteredNetworkNodes = createSelector(
networkNodesSelector,
searchQuerySelector,
(nodes, searchQuery) => filter(nodes, node => node.alias.includes(searchQuery) || node.pub_key.includes(searchQuery))
)
export { friendFormSelectors }
// ------------------------------------
// Reducer
// ------------------------------------
export default function friendFormReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

3
app/reducers/index.js

@ -10,6 +10,8 @@ import peers from './peers'
import channels from './channels' import channels from './channels'
import channelform from './channelform' import channelform from './channelform'
import friendsform from './friendsform'
import form from './form' import form from './form'
import payform from './payform' import payform from './payform'
import requestform from './requestform' import requestform from './requestform'
@ -32,6 +34,7 @@ const rootReducer = combineReducers({
peers, peers,
channels, channels,
channelform, channelform,
friendsform,
form, form,
payform, payform,

100
app/routes/friends/components/Friends.js

@ -6,44 +6,55 @@ import Isvg from 'react-inlinesvg'
import { MdSearch } from 'react-icons/lib/md' import { MdSearch } from 'react-icons/lib/md'
import { FaCircle } from 'react-icons/lib/fa' import { FaCircle } from 'react-icons/lib/fa'
import { btc } from 'utils'
import FriendsForm from 'components/Friends/FriendsForm'
import OnlineContact from 'components/Friends/OnlineContact'
import PendingContact from 'components/Friends/PendingContact'
import ClosingContact from 'components/Friends/ClosingContact'
import OfflineContact from 'components/Friends/OfflineContact'
import plus from 'icons/plus.svg' import plus from 'icons/plus.svg'
import styles from './Friends.scss' import styles from './Friends.scss'
class Friends extends Component { class Friends extends Component {
constructor(props) {
super(props)
}
componentWillMount() { componentWillMount() {
const { fetchChannels, fetchPeers } = this.props const { fetchChannels, fetchPeers, fetchDescribeNetwork } = this.props
fetchChannels() fetchChannels()
fetchPeers() fetchPeers()
fetchDescribeNetwork()
} }
render() { render() {
const { const {
channels, channels,
currentChannels,
activeChannels, activeChannels,
nonActiveChannels, nonActiveChannels,
pendingOpenChannels, pendingOpenChannels,
closingPendingChannels,
openFriendsForm,
friendsFormProps,
peers peers
} = this.props } = this.props
console.log('pendingOpenChannels: ', pendingOpenChannels)
return ( return (
<div className={styles.friendsContainer}> <div className={styles.friendsContainer}>
<FriendsForm {...friendsFormProps} />
<header className={styles.header}> <header className={styles.header}>
<div className={styles.titleContainer}> <div className={styles.titleContainer}>
<div className={styles.left}> <div className={styles.left}>
<h1>Friends ({activeChannels.length} online)</h1> <h1>Contacts <span>({activeChannels.length} online)</span></h1>
</div> </div>
</div> </div>
<div className={styles.newFriendContainer}> <div className={styles.newFriendContainer}>
<div className={`buttonPrimary ${styles.newFriendButton}`} onClick={() => (console.log('yo'))}> <div className={`buttonPrimary ${styles.newFriendButton}`} onClick={openFriendsForm}>
<Isvg src={plus} /> <Isvg src={plus} />
<span>Add</span> <span>Add</span>
</div> </div>
@ -66,65 +77,18 @@ class Friends extends Component {
<ul className={styles.friends}> <ul className={styles.friends}>
{ {
activeChannels.length > 0 && activeChannels.map(activeChannel => { currentChannels.length > 0 && currentChannels.map(channel => {
console.log('activeChannel: ', activeChannel) console.log('channel: ', channel)
return (
<li className={styles.friend} key={activeChannel.chan_id}> if (channel.active) {
<section className={styles.info}> return <OnlineContact channel={channel} />
<p className={styles.online}> } else if (!channel.active) {
<FaCircle style={{ verticalAlign: 'top' }} /> return <OfflineContact channel={channel} />
<span>Online</span> } else if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
</p> return <PendingContact channel={channel} />
<h2>{activeChannel.remote_pubkey}</h2> } else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
</section> return <ClosingContact channel={channel} />
<section> }
</section>
</li>
)
})
}
{
pendingOpenChannels.length > 0 && pendingOpenChannels.map(pendingOpenChannel => {
console.log('pendingOpenChannel: ', pendingOpenChannel)
return (
<li className={styles.friend} key={pendingOpenChannel.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/${pendingOpenChannel.channel.channel_point.split(':')[0]}`)}>
(~{pendingOpenChannel.blocks_till_open * 10} minutes)
</i>
</span>
</p>
<h2>{pendingOpenChannel.channel.remote_node_pub}</h2>
</section>
<section>
</section>
</li>
)
})
}
{
nonActiveChannels.length > 0 && nonActiveChannels.map(nonActiveChannel => {
console.log('nonActiveChannel: ', nonActiveChannel)
return (
<li className={styles.friend} key={nonActiveChannel.chan_id}>
<section className={styles.info}>
<p>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>Offline</span>
</p>
<h2>{nonActiveChannel.remote_pubkey}</h2>
</section>
<section>
</section>
</li>
)
}) })
} }
</ul> </ul>

46
app/routes/friends/components/Friends.scss

@ -16,6 +16,12 @@
text-transform: uppercase; text-transform: uppercase;
font-size: 26px; font-size: 26px;
margin-right: 5px; margin-right: 5px;
span {
display: inline-block;
vertical-align: middle;
font-size: 16px;
}
} }
} }
} }
@ -82,9 +88,27 @@
} }
.friend { .friend {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 30px 0; padding: 30px 0;
border-bottom: 1px solid $traditionalgrey; border-bottom: 1px solid $traditionalgrey;
.limits {
display: flex;
flex-direction: row;
justify-content: space-between;
div {
margin: 0 10px;
h4 {
font-size: 12px;
margin-bottom: 20px;
}
}
}
.info { .info {
p { p {
margin-bottom: 20px; margin-bottom: 20px;
@ -98,10 +122,28 @@
} }
&.pending { &.pending {
color: #FF8A65; color: $orange;
svg {
color: $orange;
}
i {
margin-left: 5px;
color: $darkestgrey;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
&.closing {
color: $red;
svg { svg {
color: #FF8A65; color: $red;
} }
i { i {

60
app/routes/friends/containers/FriendsContainer.js

@ -1,24 +1,76 @@
import { withRouter } from 'react-router' import { withRouter } from 'react-router'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { fetchChannels, channelsSelectors } from 'reducers/channels' import {
fetchChannels,
openChannel,
currentChannels,
channelsSelectors
} from 'reducers/channels'
import { fetchPeers } from 'reducers/peers' import { fetchPeers } from 'reducers/peers'
import { fetchDescribeNetwork } from 'reducers/network'
import {
openFriendsForm,
closeFriendsForm,
updateFriendFormSearchQuery,
friendFormSelectors
} from 'reducers/friendsform'
import Friends from '../components/Friends' import Friends from '../components/Friends'
const mapDispatchToProps = { const mapDispatchToProps = {
openFriendsForm,
closeFriendsForm,
updateFriendFormSearchQuery,
openChannel,
fetchChannels, fetchChannels,
fetchPeers fetchPeers,
fetchDescribeNetwork
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
channels: state.channels, channels: state.channels,
peers: state.peers, peers: state.peers,
network: state.network,
friendsform: state.friendsform,
currentChannels: currentChannels(state),
activeChannels: channelsSelectors.activeChannels(state), activeChannels: channelsSelectors.activeChannels(state),
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state),
nonActiveChannels: channelsSelectors.nonActiveChannels(state), nonActiveChannels: channelsSelectors.nonActiveChannels(state),
pendingOpenChannels: channelsSelectors.pendingOpenChannels(state) nonActiveChannelPubkeys: channelsSelectors.nonActiveChannelPubkeys(state),
pendingOpenChannels: channelsSelectors.pendingOpenChannels(state),
pendingOpenChannelPubkeys: channelsSelectors.pendingOpenChannelPubkeys(state),
closingPendingChannels: channelsSelectors.closingPendingChannels(state),
filteredNetworkNodes: friendFormSelectors.filteredNetworkNodes(state)
}) })
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Friends)) const mergeProps = (stateProps, dispatchProps, ownProps) => {
const friendsFormProps = {
closeFriendsForm: dispatchProps.closeFriendsForm,
updateFriendFormSearchQuery: dispatchProps.updateFriendFormSearchQuery,
openChannel: dispatchProps.openChannel,
friendsform: stateProps.friendsform,
filteredNetworkNodes: stateProps.filteredNetworkNodes,
activeChannelPubkeys: stateProps.activeChannelPubkeys,
nonActiveChannelPubkeys: stateProps.nonActiveChannelPubkeys,
pendingOpenChannelPubkeys: stateProps.pendingOpenChannelPubkeys
}
return {
...stateProps,
...dispatchProps,
...ownProps,
friendsFormProps
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Friends))

1
app/variables.scss

@ -14,4 +14,5 @@ $green: #0bb634;
$terminalgreen: #00FF00; $terminalgreen: #00FF00;
$red: #ff0b00; $red: #ff0b00;
$blue: #007bb6; $blue: #007bb6;
$orange: #FF8A65;
$curve: cubic-bezier(0.650, 0.000, 0.450, 1.000); $curve: cubic-bezier(0.650, 0.000, 0.450, 1.000);
Loading…
Cancel
Save