diff --git a/app/components/Contacts/AddChannel.js b/app/components/Contacts/AddChannel.js new file mode 100644 index 00000000..92a23799 --- /dev/null +++ b/app/components/Contacts/AddChannel.js @@ -0,0 +1,154 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Isvg from 'react-inlinesvg' +import { FaCircle, FaQuestionCircle } from 'react-icons/lib/fa' + +import x from 'icons/x.svg' + +import styles from './AddChannel.scss' + +const AddChannel = ({ + contactsform, + contactsform: { showErrors }, + closeContactsForm, + openSubmitChannelForm, + updateContactFormSearchQuery, + updateManualFormSearchQuery, + updateContactCapacity, + setPubkey, + openChannel, + updateManualFormErrors, + activeChannelPubkeys, + nonActiveChannelPubkeys, + pendingOpenChannelPubkeys, + filteredNetworkNodes, + loadingChannelPubkeys, + showManualForm, + manualFormIsValid +}) => { + + const renderRightSide = (node) => { + if (loadingChannelPubkeys.includes(node.pub_key)) { + return ( + +
+
+
+ + ) + } + + 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 ( + { + // set the node public key for the submit form + setPubkey(node.pub_key) + // open the submit form + openSubmitChannelForm() + }} + > + Connect + + ) + } + + const searchUpdated = (search) => { + updateContactFormSearchQuery(search) + + if (search.includes('@') && search.split('@')[0].length === 66) { + updateManualFormSearchQuery(search) + } + } + + return ( +
+
+ searchUpdated(event.target.value)} + ref={input => input && input.focus()} + /> + + + +
+ +
+
    + { + filteredNetworkNodes.map(node => ( +
  • +
    + { + 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)} +
    +
  • + )) + } +
+
+ + { + showManualForm && +
+

Hm, looks like we can't see that node from here, wanna try to manually connect?

+
Connect Manually
+
+ } +
+ ) +} + +AddChannel.propTypes = { + +} + +export default AddChannel diff --git a/app/components/Contacts/AddChannel.scss b/app/components/Contacts/AddChannel.scss new file mode 100644 index 00000000..eda5f64a --- /dev/null +++ b/app/components/Contacts/AddChannel.scss @@ -0,0 +1,141 @@ +@import '../../variables.scss'; + +.container { + position: relative; + width: 30%; + display: inline-block; + vertical-align: top; + height: 100vh; + background: #31343f; +} + +.header { + display: flex; + flex-direction: row; + justify-content: space-between; + background: linear-gradient(270deg, #868B9F 0%, #333C5E 100%); + padding: 15px 10px; + color: $white; + + input { + background: transparent; + outline: 0; + border: 0; + color: $white; + font-size: 14px; + width: 90%; + } + + .closeIcon { + cursor: pointer; + transition: all 0.25s; + + &:hover { + opacity: 0.5; + } + + svg { + height: 14px; + width: 14px; + } + } +} + +.nodes { + background: #31343F; + + .networkResults { + overflow-y: auto; + margin-top: 30px; + padding: 0 10px; + color: $white; + + li { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 0; + + h2 { + font-size: 10px; + font-weight: bold; + letter-spacing: 1.3px; + + span { + display: inline-block; + vertical-align: middle; + + &:nth-child(1) { + font-size: 10px; + font-weight: bold; + letter-spacing: 1.3px; + } + + &:nth-child(2) { + display: block; + color: $darkestgrey; + font-size: 8px; + margin-top: 5px; + } + } + } + + .connect { + cursor: pointer; + color: $darkestgrey; + transition: all 0.25s; + font-size: 10px; + + &:hover { + color: darken($darkestgrey, 10%); + } + } + + .inactive { + font-size: 10px; + + display: inline-block; + vertical-align: top; + + &.online { + color: $green; + } + + &.offline { + color: $darkestgrey; + } + + &.pending { + color: $orange; + } + + &.private { + color: darken($darkestgrey, 50%); + } + } + } + } +} + +.manualForm { + color: $white; + text-align: center; + margin: 0 25px; + + p { + font-size: 14px; + margin: 20px 0; + } + + div { + background: #383B47; + font-size: 16px; + padding: 10px; + cursor: pointer; + transition: all 0.25s; + + &:hover { + background: lighten(#383B47, 10%); + } + } +} \ No newline at end of file diff --git a/app/components/Contacts/Network.js b/app/components/Contacts/Network.js index 13a3c61f..8bba7417 100644 --- a/app/components/Contacts/Network.js +++ b/app/components/Contacts/Network.js @@ -6,6 +6,7 @@ import { FaAngleDown, FaCircle, FaRepeat } from 'react-icons/lib/fa' import { btc } from 'utils' import plus from 'icons/plus.svg' import search from 'icons/search.svg' + import styles from './Network.scss' class Network extends Component { @@ -100,7 +101,9 @@ class Network extends Component {
- + + +
diff --git a/app/components/Contacts/Network.scss b/app/components/Contacts/Network.scss index 7f9332e8..0eb56a29 100644 --- a/app/components/Contacts/Network.scss +++ b/app/components/Contacts/Network.scss @@ -2,7 +2,7 @@ .network { position: relative; - width: 20%; + width: 30%; display: inline-block; vertical-align: top; height: 100vh; @@ -33,8 +33,16 @@ cursor: pointer; transition: all 0.25s; + svg { + border-radius: 5px; + } + &:hover { - color: $darkestgrey; + opacity: 0.5; + + svg { + background: #272931; + } } } } diff --git a/app/components/Contacts/SubmitChannelForm.js b/app/components/Contacts/SubmitChannelForm.js new file mode 100644 index 00000000..e67f822b --- /dev/null +++ b/app/components/Contacts/SubmitChannelForm.js @@ -0,0 +1,89 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { FaAngleDown } from 'react-icons/lib/fa' +import Isvg from 'react-inlinesvg' +import x from 'icons/x.svg' + +import styles from './SubmitChannelForm.scss' + +class SubmitChannelForm extends React.Component { + render() { + const { + submitChannelFormOpen, + closeSubmitChannelForm, + + pubkey, + contactCapacity, + updateContactCapacity, + + toggleCurrencyProps: { + setContactsCurrencyFilters, + setCurrencyFilters, + showCurrencyFilters, + currencyName, + currentCurrencyFilters, + onCurrencyFilterClick, + contactFormUsdAmount + } + } = this.props + + if (!submitChannelFormOpen) { return null } + + return ( +
+
+ + + +
+ +
+
+

Add Funds to Network

+

Adding a connection will help you send and receive money on the Lightning Network. You aren't spening any money, rather moving the money you plan to use onto the network.

+
+ +
+

{pubkey}

+
+ +
+
+ { this.amountInput = input }} + size='' + placeholder='0.00000000' + value={contactCapacity || ''} + onChange={event => updateContactCapacity(event.target.value)} + // onBlur={onPayAmountBlur} + id='amount' + /> +
+
setContactsCurrencyFilters(!showCurrencyFilters)}> + {currencyName} +
+
    + { + currentCurrencyFilters.map(filter => +
  • onCurrencyFilterClick(filter.key)}>{filter.name}
  • ) + } +
+
+
+ +
+ {`≈ ${contactFormUsdAmount || 0} USD`} +
+
+
+
+ ) + } +} + +SubmitChannelForm.propTypes = {} + +export default SubmitChannelForm diff --git a/app/components/Contacts/SubmitChannelForm.scss b/app/components/Contacts/SubmitChannelForm.scss new file mode 100644 index 00000000..114bcbb0 --- /dev/null +++ b/app/components/Contacts/SubmitChannelForm.scss @@ -0,0 +1,154 @@ +@import '../../variables.scss'; + +.container { + position: absolute; + top: 0; + z-index: 10; + height: 100vh; + width: 100%; + background: #31343F; +} + +.closeContainer { + text-align: right; + padding: 20px 40px 0px; + + span { + cursor: pointer; + opacity: 1.0; + transition: 0.25s all; + + &:hover { + opacity: 0.5; + } + } + + svg { + color: $white; + } +} + + +.content { + padding: 0 40px; + font-family: Roboto; + color: $white; + + .header { + padding: 20px 100px; + + h1 { + margin-bottom: 15px; + font-size: 20px; + } + + p { + text-align: left; + line-height: 1.3; + font-size: 12px; + } + } +} + +.header { + text-align: center; + padding-bottom: 20px; + border-bottom: 1px solid $spaceborder; + + h1 { + font-size: 22px; + font-weight: 100; + margin-top: 10px; + letter-spacing: 1.5px; + } +} + +.title { + margin: 50px 0; + + h2 { + font-size: 14px; + background: $spaceblue; + padding: 10px; + border-radius: 17.5px; + display: inline; + } +} + +.input { + display: flex; + flex-direction: row; + align-items: center; + + input { + font-size: 40px; + max-width: 230px; + } +} + +.input input { + background: transparent; + outline: none; + border: 0; + color: $gold; + -webkit-text-fill-color: $white; + width: 100%; + font-weight: 200; +} + +.input input::-webkit-input-placeholder, ::-webkit-input-placeholder { + text-shadow: none; + -webkit-text-fill-color: initial; +} + +.currency { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + + .currentCurrency { + cursor: pointer; + transition: 0.25s all; + + &:hover { + opacity: 0.5; + } + + span { + font-size: 14px; + + &:nth-child(1) { + font-weight: bold; + } + } + + } + + ul { + visibility: hidden; + position: absolute; + top: 30px; + + &.active { + visibility: visible; + } + + li { + padding: 8px 15px; + background: #191919; + cursor: pointer; + transition: 0.25s hover; + border-bottom: 1px solid #0f0f0f; + + &:hover { + background: #0f0f0f; + } + } + } +} + +.usdAmount { + margin-top: 20px; + opacity: 0.5; +} diff --git a/app/components/Form/Form.js b/app/components/Form/Form.js index 9346fbd5..5a5c6892 100644 --- a/app/components/Form/Form.js +++ b/app/components/Form/Form.js @@ -19,7 +19,7 @@ const Form = ({ formType, formProps, closeForm }) => { const FormComponent = FORM_TYPES[formType] return ( -
+
diff --git a/app/main.dev.js b/app/main.dev.js index e3b54bba..4e5dfcf3 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -200,7 +200,7 @@ const startLnd = (alias, autopilot) => { }, 1000) } - if (line.includes('The wallet has been unlocked')) { + if (line.includes('LightningWallet opened')) { console.log('WALLET OPENED, STARTING LIGHTNING GRPC CONNECTION') sendLndSyncing() startGrpc() diff --git a/app/reducers/contactsform.js b/app/reducers/contactsform.js index f09144b2..47648a45 100644 --- a/app/reducers/contactsform.js +++ b/app/reducers/contactsform.js @@ -2,15 +2,24 @@ import { createSelector } from 'reselect' import filter from 'lodash/filter' import isEmpty from 'lodash/isEmpty' +import { tickerSelectors } from './ticker' +import { btc } from '../utils' + // Initial State const initialState = { isOpen: false, searchQuery: '', manualSearchQuery: '', contactCapacity: 0.1, + pubkey: '', showErrors: { manualInput: false - } + }, + + manualFormOpen: false, + submitChannelFormOpen: false, + + showCurrencyFilters: false } // Constants @@ -18,6 +27,14 @@ const initialState = { export const OPEN_CONTACTS_FORM = 'OPEN_CONTACTS_FORM' export const CLOSE_CONTACTS_FORM = 'CLOSE_CONTACTS_FORM' +export const OPEN_MANUAL_FORM = 'OPEN_MANUAL_FORM' +export const CLOSE_MANUAL_FORM = 'CLOSE_MANUAL_FORM' + +export const OPEN_SUBMIT_CHANNEL_FORM = 'OPEN_SUBMIT_CHANNEL_FORM' +export const CLOSE_SUBMIT_CHANNEL_FORM = 'CLOSE_SUBMIT_CHANNEL_FORM' + +export const SET_PUBKEY = 'SET_PUBKEY' + export const UPDATE_CONTACT_FORM_SEARCH_QUERY = 'UPDATE_CONTACT_FORM_SEARCH_QUERY' export const UPDATE_CONTACT_CAPACITY = 'UPDATE_CONTACT_CAPACITY' @@ -26,6 +43,8 @@ export const UPDATE_MANUAL_FORM_ERRORS = 'UPDATE_MANUAL_FORM_ERRORS' export const UPDATE_MANUAL_FORM_SEARCH_QUERY = 'UPDATE_MANUAL_FORM_SEARCH_QUERY' +export const SET_CONTACTS_CURRENCY_FILTERS = 'SET_CONTACTS_CURRENCY_FILTERS' + // ------------------------------------ // Actions // ------------------------------------ @@ -41,6 +60,30 @@ export function closeContactsForm() { } } +export function openManualForm() { + return { + type: OPEN_MANUAL_FORM + } +} + +export function closeManualForm() { + return { + type: CLOSE_MANUAL_FORM + } +} + +export function openSubmitChannelForm() { + return { + type: OPEN_SUBMIT_CHANNEL_FORM + } +} + +export function closeSubmitChannelForm() { + return { + type: CLOSE_SUBMIT_CHANNEL_FORM + } +} + export function updateContactFormSearchQuery(searchQuery) { return { type: UPDATE_CONTACT_FORM_SEARCH_QUERY, @@ -62,6 +105,13 @@ export function updateContactCapacity(contactCapacity) { } } +export function setPubkey(pubkey) { + return { + type: SET_PUBKEY, + pubkey + } +} + export function updateManualFormErrors(errorsObject) { return { type: UPDATE_MANUAL_FORM_ERRORS, @@ -69,6 +119,13 @@ export function updateManualFormErrors(errorsObject) { } } +export function setContactsCurrencyFilters(showCurrencyFilters) { + return { + type: SET_CONTACTS_CURRENCY_FILTERS, + showCurrencyFilters + } +} + // ------------------------------------ // Action Handlers // ------------------------------------ @@ -76,15 +133,25 @@ const ACTION_HANDLERS = { [OPEN_CONTACTS_FORM]: state => ({ ...state, isOpen: true }), [CLOSE_CONTACTS_FORM]: state => ({ ...state, isOpen: false }), + [OPEN_MANUAL_FORM]: state => ({ ...state, manualFormOpen: true }), + [CLOSE_MANUAL_FORM]: state => ({ ...state, manualFormOpen: false }), + + [OPEN_SUBMIT_CHANNEL_FORM]: state => ({ ...state, submitChannelFormOpen: true }), + [CLOSE_SUBMIT_CHANNEL_FORM]: state => ({ ...state, submitChannelFormOpen: false }), + [UPDATE_CONTACT_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }), [UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }), [UPDATE_CONTACT_CAPACITY]: (state, { contactCapacity }) => ({ ...state, contactCapacity }), + + [SET_PUBKEY]: (state, { pubkey }) => ({ ...state, pubkey }), [UPDATE_MANUAL_FORM_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }), - [UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { manualSearchQuery }) => ({ ...state, manualSearchQuery }) + [UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { manualSearchQuery }) => ({ ...state, manualSearchQuery }), + + [SET_CONTACTS_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ({ ...state, showCurrencyFilters }) } // ------------------------------------ @@ -94,6 +161,8 @@ const contactFormSelectors = {} const networkNodesSelector = state => state.network.nodes const searchQuerySelector = state => state.contactsform.searchQuery const manualSearchQuerySelector = state => state.contactsform.manualSearchQuery +const contactCapacitySelector = state => state.contactsform.contactCapacity +const currencySelector = state => state.ticker.currency const contactable = node => ( node.addresses.length > 0 @@ -115,7 +184,10 @@ contactFormSelectors.filteredNetworkNodes = createSelector( (nodes, searchQuery) => { // If there is no search query default to showing the first 20 nodes from the nodes array // (performance hit to render the entire thing by default) - if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) } + // if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) } + + // return an empty array if there is no search query + if (!searchQuery.length) { return [] } // if there is an '@' in the search query we are assuming they are using the format pubkey@host // we can ignore the '@' and the host and just grab the pubkey for our search @@ -153,6 +225,17 @@ contactFormSelectors.manualFormIsValid = createSelector( } ) +contactFormSelectors.contactFormUsdAmount = createSelector( + contactCapacitySelector, + currencySelector, + tickerSelectors.currentTicker, + (amount, currency, ticker) => { + if (!ticker || !ticker.price_usd) { return false } + + return btc.convert(currency, 'usd', amount, ticker.price_usd) + } +) + export { contactFormSelectors } diff --git a/app/routes/app/components/App.js b/app/routes/app/components/App.js index f80e44b2..420961c0 100644 --- a/app/routes/app/components/App.js +++ b/app/routes/app/components/App.js @@ -8,9 +8,12 @@ import Form from 'components/Form' import ModalRoot from 'components/ModalRoot' import Network from 'components/Contacts/Network' +import AddChannel from 'components/Contacts/AddChannel' import ContactModal from 'components/Contacts/ContactModal' import ContactsForm from 'components/Contacts/ContactsForm' +import SubmitChannelForm from 'components/Contacts/SubmitChannelForm' + import ReceiveModal from 'components/Wallet/ReceiveModal' import ActivityModal from 'components/Activity/ActivityModal' @@ -60,6 +63,7 @@ class App extends Component { networkTabProps, receiveModalProps, activityModalProps, + submitChannelFormProps, children } = this.props @@ -79,9 +83,9 @@ class App extends Component { /> -
+ @@ -90,7 +94,12 @@ class App extends Component { {children}
- + { + contactsFormProps.contactsform.isOpen ? + + : + + }
) } diff --git a/app/routes/app/components/App.scss b/app/routes/app/components/App.scss index d12cf532..f7472067 100644 --- a/app/routes/app/components/App.scss +++ b/app/routes/app/components/App.scss @@ -2,7 +2,7 @@ .content { position: relative; - width: 80%; + width: 70%; display: inline-block; vertical-align: top; overflow-y: auto; diff --git a/app/routes/app/containers/AppContainer.js b/app/routes/app/containers/AppContainer.js index cc7459c8..435e7d93 100644 --- a/app/routes/app/containers/AppContainer.js +++ b/app/routes/app/containers/AppContainer.js @@ -1,6 +1,8 @@ import { withRouter } from 'react-router' import { connect } from 'react-redux' +import { btc } from 'utils' + import { fetchTicker, setCurrency, tickerSelectors } from 'reducers/ticker' import { newAddress, closeWalletModal } from 'reducers/address' @@ -39,11 +41,19 @@ import { import { openContactsForm, closeContactsForm, + + openSubmitChannelForm, + closeSubmitChannelForm, + updateContactFormSearchQuery, updateManualFormSearchQuery, updateContactCapacity, + setPubkey, + contactFormSelectors, - updateManualFormErrors + updateManualFormErrors, + + setContactsCurrencyFilters } from 'reducers/contactsform' import { fetchBalance } from 'reducers/balance' @@ -100,11 +110,15 @@ const mapDispatchToProps = { openContactsForm, closeContactsForm, + openSubmitChannelForm, + closeSubmitChannelForm, updateContactFormSearchQuery, updateManualFormSearchQuery, updateContactCapacity, + setPubkey, contactFormSelectors, updateManualFormErrors, + setContactsCurrencyFilters, fetchDescribeNetwork, @@ -155,6 +169,7 @@ const mapStateToProps = state => ({ filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state), showManualForm: contactFormSelectors.showManualForm(state), manualFormIsValid: contactFormSelectors.manualFormIsValid(state), + contactFormUsdAmount: contactFormSelectors.contactFormUsdAmount(state), currentChannels: currentChannels(state), activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state), @@ -292,9 +307,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { const contactsFormProps = { closeContactsForm: dispatchProps.closeContactsForm, + openSubmitChannelForm: dispatchProps.openSubmitChannelForm, updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery, updateManualFormSearchQuery: dispatchProps.updateManualFormSearchQuery, updateContactCapacity: dispatchProps.updateContactCapacity, + setPubkey: dispatchProps.setPubkey, openChannel: dispatchProps.openChannel, updateManualFormErrors: dispatchProps.updateManualFormErrors, @@ -348,6 +365,30 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { closeReceiveModal: dispatchProps.closeWalletModal } + const submitChannelFormProps = { + submitChannelFormOpen: stateProps.contactsform.submitChannelFormOpen, + pubkey: stateProps.contactsform.pubkey, + contactCapacity: stateProps.contactsform.contactCapacity, + + closeSubmitChannelForm: dispatchProps.closeSubmitChannelForm, + updateContactCapacity: dispatchProps.updateContactCapacity, + + toggleCurrencyProps: { + currentCurrencyFilters: stateProps.currentCurrencyFilters, + currencyName: stateProps.currencyName, + showCurrencyFilters: stateProps.contactsform.showCurrencyFilters, + contactFormUsdAmount: stateProps.contactFormUsdAmount, + + setContactsCurrencyFilters: dispatchProps.setContactsCurrencyFilters, + setCurrencyFilters: dispatchProps.setCurrencyFilters, + onCurrencyFilterClick: (currency) => { + dispatchProps.updateContactCapacity(btc.convert(stateProps.ticker.currency, currency, stateProps.contactsform.contactCapacity)) + dispatchProps.setCurrency(currency) + dispatchProps.setContactsCurrencyFilters(false) + } + } + } + return { ...stateProps, ...dispatchProps, @@ -363,6 +404,8 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { receiveModalProps, // props for the activity modals activityModalProps, + // props for the for to open a channel + submitChannelFormProps, // Props to pass to the pay form formProps: formProps(stateProps.form.formType), // action to close form