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 find from 'lodash/find'
import { listPeers, connectPeer } from './peersController'
import pushopenchannel from '../push/openchannel'
import pushclosechannel from '../push/closechannel'
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.
* @param {[type]} lnd [description]
@ -12,6 +64,7 @@ const BufferUtil = bitcore.util.buffer
* @return {[type]} [description]
*/
export function openChannel(lnd, meta, event, payload) {
console.log('opening the channel')
const { pubkey, localamt, pushamt } = payload
const res = {
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))
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:
}
}

2
app/lnd/methods/peersController.js

@ -7,7 +7,7 @@
*/
export function connectPeer(lnd, meta, { pubkey, host }) {
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) }
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 })
// 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 pushamt = btc.btcToSatoshis(push_amt)
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
@ -293,56 +292,74 @@ channelsSelectors.activeChannels = createSelector(
openChannels => openChannels.filter(channel => channel.active)
)
channelsSelectors.activeChannelPubkeys = createSelector(
channelsSelector,
openChannels => openChannels.filter(channel => channel.active).map(c => c.remote_pubkey)
)
channelsSelectors.nonActiveChannels = createSelector(
channelsSelector,
openChannels => openChannels.filter(channel => !channel.active)
)
channelsSelectors.nonActiveChannelPubkeys = createSelector(
channelsSelector,
openChannels => openChannels.filter(channel => !channel.active).map(c => c.remote_pubkey)
)
channelsSelectors.pendingOpenChannels = createSelector(
pendingOpenChannelsSelector,
pendingOpenChannels => pendingOpenChannels
)
const closingPendingChannels = createSelector(
channelsSelectors.pendingOpenChannelPubkeys = createSelector(
pendingOpenChannelsSelector,
pendingOpenChannels => pendingOpenChannels.map(pendingChannel => pendingChannel.channel.remote_node_pub)
)
channelsSelectors.closingPendingChannels = createSelector(
pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector,
(pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels]
)
const allChannels = createSelector(
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)
)
const allChannels = createSelector(
channelsSelectors.activeChannels,
channelsSelectors.nonActiveChannels,
pendingOpenChannelsSelector,
pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector,
channelSearchQuerySelector,
(channels, pendingOpenChannels, pendingClosedChannels, pendingForcedClosedChannels, searchQuery) => {
const filteredChannels = channels.filter(channel => channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)) // eslint-disable-line
(activeChannels, nonActiveChannels, pendingOpenChannels, pendingClosedChannels, pendingForcedClosedChannels, searchQuery) => {
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 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
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(
allChannels,
channelsSelectors.activeChannels,
channelsSelector,
pendingOpenChannelsSelector,
closingPendingChannels,
channelsSelectors.closingPendingChannels,
filterSelector,
channelSearchQuerySelector,
(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 channelform from './channelform'
import friendsform from './friendsform'
import form from './form'
import payform from './payform'
import requestform from './requestform'
@ -32,6 +34,7 @@ const rootReducer = combineReducers({
peers,
channels,
channelform,
friendsform,
form,
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 { 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 styles from './Friends.scss'
class Friends extends Component {
constructor(props) {
super(props)
}
componentWillMount() {
const { fetchChannels, fetchPeers } = this.props
const { fetchChannels, fetchPeers, fetchDescribeNetwork } = this.props
fetchChannels()
fetchPeers()
fetchDescribeNetwork()
}
render() {
const {
channels,
currentChannels,
activeChannels,
nonActiveChannels,
pendingOpenChannels,
closingPendingChannels,
openFriendsForm,
friendsFormProps,
peers
} = this.props
console.log('pendingOpenChannels: ', pendingOpenChannels)
return (
<div className={styles.friendsContainer}>
<FriendsForm {...friendsFormProps} />
<header className={styles.header}>
<div className={styles.titleContainer}>
<div className={styles.left}>
<h1>Friends ({activeChannels.length} online)</h1>
<h1>Contacts <span>({activeChannels.length} online)</span></h1>
</div>
</div>
<div className={styles.newFriendContainer}>
<div className={`buttonPrimary ${styles.newFriendButton}`} onClick={() => (console.log('yo'))}>
<div className={`buttonPrimary ${styles.newFriendButton}`} onClick={openFriendsForm}>
<Isvg src={plus} />
<span>Add</span>
</div>
@ -66,65 +77,18 @@ class Friends extends Component {
<ul className={styles.friends}>
{
activeChannels.length > 0 && activeChannels.map(activeChannel => {
console.log('activeChannel: ', activeChannel)
return (
<li className={styles.friend} key={activeChannel.chan_id}>
<section className={styles.info}>
<p className={styles.online}>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>Online</span>
</p>
<h2>{activeChannel.remote_pubkey}</h2>
</section>
<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>
)
currentChannels.length > 0 && currentChannels.map(channel => {
console.log('channel: ', channel)
if (channel.active) {
return <OnlineContact channel={channel} />
} else if (!channel.active) {
return <OfflineContact channel={channel} />
} else if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
return <PendingContact channel={channel} />
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
return <ClosingContact channel={channel} />
}
})
}
</ul>

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

@ -16,6 +16,12 @@
text-transform: uppercase;
font-size: 26px;
margin-right: 5px;
span {
display: inline-block;
vertical-align: middle;
font-size: 16px;
}
}
}
}
@ -82,9 +88,27 @@
}
.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;
@ -98,10 +122,28 @@
}
&.pending {
color: #FF8A65;
color: $orange;
svg {
color: $orange;
}
i {
margin-left: 5px;
color: $darkestgrey;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
&.closing {
color: $red;
svg {
color: #FF8A65;
color: $red;
}
i {

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

@ -1,24 +1,76 @@
import { withRouter } from 'react-router'
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 { fetchDescribeNetwork } from 'reducers/network'
import {
openFriendsForm,
closeFriendsForm,
updateFriendFormSearchQuery,
friendFormSelectors
} from 'reducers/friendsform'
import Friends from '../components/Friends'
const mapDispatchToProps = {
openFriendsForm,
closeFriendsForm,
updateFriendFormSearchQuery,
openChannel,
fetchChannels,
fetchPeers
fetchPeers,
fetchDescribeNetwork
}
const mapStateToProps = state => ({
channels: state.channels,
peers: state.peers,
network: state.network,
friendsform: state.friendsform,
currentChannels: currentChannels(state),
activeChannels: channelsSelectors.activeChannels(state),
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(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;
$red: #ff0b00;
$blue: #007bb6;
$orange: #FF8A65;
$curve: cubic-bezier(0.650, 0.000, 0.450, 1.000);
Loading…
Cancel
Save