From 1b14242e93c2adcc24ac03ae3ac0dfff2e32560e Mon Sep 17 00:00:00 2001 From: Jack Mallers Date: Thu, 4 Jan 2018 11:34:15 -0600 Subject: [PATCH] feature(contacts): start contacts feature --- app/components/Friends/ClosingContact.js | 38 +++++ app/components/Friends/Contact.scss | 102 ++++++++++++ app/components/Friends/Donut.js | 31 ++++ app/components/Friends/Donut.scss | 20 +++ app/components/Friends/FriendsForm.js | 146 ++++++++++++++++++ app/components/Friends/FriendsForm.scss | 135 ++++++++++++++++ app/components/Friends/OfflineContact.js | 33 ++++ app/components/Friends/OnlineContact.js | 33 ++++ app/components/Friends/OnlineContact.scss | 0 app/components/Friends/PendingContact.js | 38 +++++ app/lnd/methods/channelController.js | 53 +++++++ app/lnd/methods/index.js | 13 ++ app/lnd/methods/peersController.js | 2 +- app/reducers/channels.js | 57 ++++--- app/reducers/friendsform.js | 74 +++++++++ app/reducers/index.js | 3 + app/routes/friends/components/Friends.js | 100 ++++-------- app/routes/friends/components/Friends.scss | 46 +++++- .../friends/containers/FriendsContainer.js | 60 ++++++- app/variables.scss | 1 + 20 files changed, 890 insertions(+), 95 deletions(-) create mode 100644 app/components/Friends/ClosingContact.js create mode 100644 app/components/Friends/Contact.scss create mode 100644 app/components/Friends/Donut.js create mode 100644 app/components/Friends/Donut.scss create mode 100644 app/components/Friends/FriendsForm.js create mode 100644 app/components/Friends/FriendsForm.scss create mode 100644 app/components/Friends/OfflineContact.js create mode 100644 app/components/Friends/OnlineContact.js create mode 100644 app/components/Friends/OnlineContact.scss create mode 100644 app/components/Friends/PendingContact.js create mode 100644 app/reducers/friendsform.js diff --git a/app/components/Friends/ClosingContact.js b/app/components/Friends/ClosingContact.js new file mode 100644 index 00000000..99046b10 --- /dev/null +++ b/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 }) => ( +
  • +
    +

    + + + Removing + shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.closing_txid}`)}> + (Details) + + +

    +

    {channel.channel.remote_node_pub}

    +
    +
    +
    +

    Can Pay

    +

    {btc.satoshisToBtc(channel.channel.local_balance)}BTC

    +
    +
    +

    Can Receive

    +

    {btc.satoshisToBtc(channel.channel.remote_balance)}BTC

    +
    +
    +
  • +) + +ClosingContact.propTypes = { + +} + +export default ClosingContact diff --git a/app/components/Friends/Contact.scss b/app/components/Friends/Contact.scss new file mode 100644 index 00000000..d8c71d01 --- /dev/null +++ b/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; + } + } + } +} diff --git a/app/components/Friends/Donut.js b/app/components/Friends/Donut.js new file mode 100644 index 00000000..f82ded9d --- /dev/null +++ b/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 ( + + + + + ) +} + +Donut.propTypes = { +} + +export default Donut diff --git a/app/components/Friends/Donut.scss b/app/components/Friends/Donut.scss new file mode 100644 index 00000000..260ee9eb --- /dev/null +++ b/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; +} diff --git a/app/components/Friends/FriendsForm.js b/app/components/Friends/FriendsForm.js new file mode 100644 index 00000000..d6984409 --- /dev/null +++ b/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 ( + + Online + + ) + } + + if (nonActiveChannelPubkeys.includes(node.pub_key)) { + return ( + + Offline + + ) + } + + if (pendingOpenChannelPubkeys.includes(node.pub_key)) { + return ( + + Pending + + ) + } + + if (!node.addresses.length) { + return ( + + Private + + ) + } + + return ( + openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: 0.1 })} + > + Connect + + ) + } + + return ( +
    + closeFriendsForm} + parentSelector={() => document.body} + className={styles.modal} + > +
    +
    +

    Add Contact

    +
    +
    + +
    +
    + +
    event.charCode === 13 && console.log('gaaaang')}> +
    + updateFriendFormSearchQuery(event.target.value)} + autoFocus + /> +
    + +
      + { + friendsform.searchQuery.length > 0 && filteredNetworkNodes.map(node => { + return ( +
    • +
      + { + node.alias.length > 0 ? +

      + {node.alias.trim()} + ({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)}) +

      + : +

      + {node.pub_key} +

      + } +
      +
      + {renderRightSide(node)} +
      +
    • + ) + }) + } +
    +
    +
    +
    + + 0.1 + + + BTC per contact + +
    +
    +
    +
    + ) +} + +FriendsForm.propTypes = { + +} + +export default FriendsForm diff --git a/app/components/Friends/FriendsForm.scss b/app/components/Friends/FriendsForm.scss new file mode 100644 index 00000000..bd675257 --- /dev/null +++ b/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; + } + } +} diff --git a/app/components/Friends/OfflineContact.js b/app/components/Friends/OfflineContact.js new file mode 100644 index 00000000..2c1cdc9a --- /dev/null +++ b/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 }) => ( +
  • +
    +

    + + Offline +

    +

    {channel.remote_pubkey}

    +
    +
    +
    +

    Can Pay

    +

    {btc.satoshisToBtc(channel.local_balance)}BTC

    +
    +
    +

    Can Receive

    +

    {btc.satoshisToBtc(channel.remote_balance)}BTC

    +
    +
    +
  • +) + +OfflineContact.propTypes = { + +} + +export default OfflineContact diff --git a/app/components/Friends/OnlineContact.js b/app/components/Friends/OnlineContact.js new file mode 100644 index 00000000..b49cc73a --- /dev/null +++ b/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 }) => ( +
  • +
    +

    + + Online +

    +

    {channel.remote_pubkey}

    +
    +
    +
    +

    Can Pay

    +

    {btc.satoshisToBtc(channel.local_balance)}BTC

    +
    +
    +

    Can Receive

    +

    {btc.satoshisToBtc(channel.remote_balance)}BTC

    +
    +
    +
  • +) + +OnlineContact.propTypes = { + +} + +export default OnlineContact diff --git a/app/components/Friends/OnlineContact.scss b/app/components/Friends/OnlineContact.scss new file mode 100644 index 00000000..e69de29b diff --git a/app/components/Friends/PendingContact.js b/app/components/Friends/PendingContact.js new file mode 100644 index 00000000..1375bcb6 --- /dev/null +++ b/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 }) => ( +
  • +
    +

    + + + Pending + shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.channel.channel_point.split(':')[0]}`)}> + (~{channel.blocks_till_open * 10} minutes) + + +

    +

    {channel.channel.remote_node_pub}

    +
    +
    +
    +

    Can Pay

    +

    {btc.satoshisToBtc(channel.channel.local_balance)}BTC

    +
    +
    +

    Can Receive

    +

    {btc.satoshisToBtc(channel.channel.remote_balance)}BTC

    +
    +
    +
  • +) + +PendingContact.propTypes = { + +} + +export default PendingContact diff --git a/app/lnd/methods/channelController.js b/app/lnd/methods/channelController.js index e2117126..40a4fdaa 100644 --- a/app/lnd/methods/channelController.js +++ b/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), diff --git a/app/lnd/methods/index.js b/app/lnd/methods/index.js index 87f5a76f..a0b9bf6e 100644 --- a/app/lnd/methods/index.js +++ b/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: } } diff --git a/app/lnd/methods/peersController.js b/app/lnd/methods/peersController.js index 2562901e..7a249366 100644 --- a/app/lnd/methods/peersController.js +++ b/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) diff --git a/app/reducers/channels.js b/app/reducers/channels.js index e6f00d23..30b9a4e9 100644 --- a/app/reducers/channels.js +++ b/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) => { diff --git a/app/reducers/friendsform.js b/app/reducers/friendsform.js new file mode 100644 index 00000000..a3e037b2 --- /dev/null +++ b/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 +} diff --git a/app/reducers/index.js b/app/reducers/index.js index 6e77afba..f3d06968 100644 --- a/app/reducers/index.js +++ b/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, diff --git a/app/routes/friends/components/Friends.js b/app/routes/friends/components/Friends.js index d7f48bdc..f29f775a 100644 --- a/app/routes/friends/components/Friends.js +++ b/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 (
    + +
    -

    Friends ({activeChannels.length} online)

    +

    Contacts ({activeChannels.length} online)

    -
    (console.log('yo'))}> +
    Add
    @@ -66,65 +77,18 @@ class Friends extends Component {
      { - activeChannels.length > 0 && activeChannels.map(activeChannel => { - console.log('activeChannel: ', activeChannel) - return ( -
    • -
      -

      - - Online -

      -

      {activeChannel.remote_pubkey}

      -
      -
      - -
      -
    • - ) - }) - } - { - pendingOpenChannels.length > 0 && pendingOpenChannels.map(pendingOpenChannel => { - console.log('pendingOpenChannel: ', pendingOpenChannel) - return ( -
    • -
      -

      - - - Pending - shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${pendingOpenChannel.channel.channel_point.split(':')[0]}`)}> - (~{pendingOpenChannel.blocks_till_open * 10} minutes) - - -

      -

      {pendingOpenChannel.channel.remote_node_pub}

      -
      -
      - -
      -
    • - ) - }) - } - { - nonActiveChannels.length > 0 && nonActiveChannels.map(nonActiveChannel => { - console.log('nonActiveChannel: ', nonActiveChannel) - return ( -
    • -
      -

      - - Offline -

      -

      {nonActiveChannel.remote_pubkey}

      -
      -
      - -
      -
    • - ) + currentChannels.length > 0 && currentChannels.map(channel => { + console.log('channel: ', channel) + + if (channel.active) { + return + } else if (!channel.active) { + return + } else if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) { + return + } else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { + return + } }) }
    diff --git a/app/routes/friends/components/Friends.scss b/app/routes/friends/components/Friends.scss index 3a17b59d..9cf4f14e 100644 --- a/app/routes/friends/components/Friends.scss +++ b/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 { diff --git a/app/routes/friends/containers/FriendsContainer.js b/app/routes/friends/containers/FriendsContainer.js index 90c23781..f5a2bde1 100644 --- a/app/routes/friends/containers/FriendsContainer.js +++ b/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)) diff --git a/app/variables.scss b/app/variables.scss index 214d8b7e..fdaf7d28 100644 --- a/app/variables.scss +++ b/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); \ No newline at end of file