diff --git a/.babelrc b/.babelrc index 6674048..0bcad69 100644 --- a/.babelrc +++ b/.babelrc @@ -1,9 +1,17 @@ { "presets": [ - "@babel/preset-env", + [ + "@babel/preset-env", + { + "targets": { + "electron": "5.0.6" + } + } + ], "@babel/preset-react" ], "plugins": [ - "@babel/plugin-transform-runtime" + "@babel/plugin-transform-runtime", + "@babel/plugin-transform-modules-commonjs" ] } diff --git a/package.json b/package.json index 770383b..4c0ea6c 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "dependencies": { "@apollo/react-hooks": "^3.1.0", "@babel/runtime": "^7.5.4", + "@nodeutils/defaults-deep": "^1.1.0", "apollo-cache-inmemory": "^1.6.3", "apollo-client": "^2.6.4", "apollo-link-http": "^1.5.16", @@ -81,13 +82,21 @@ "graphql-tag": "^2.10.1", "graphql-tools": "^4.0.5", "graphql-transport-electron": "^1.0.1", + "ipfs": "^0.38.0", "ipfs-http-client": "^33.1.0", "ipfsd-ctl": "^0.44.1", "is-ipfs": "^0.6.1", "isomorphic-fetch": "^2.2.1", "iterall": "^1.2.2", + "libp2p": "^0.26.2", + "libp2p-kad-dht": "^0.16.0", + "libp2p-mplex": "^0.8.5", + "libp2p-secio": "^0.11.1", + "libp2p-tcp": "^0.13.2", + "libp2p-webrtc-star": "^0.16.1", "lodash": "^4.17.15", "node-fetch": "^2.6.0", + "pull-stream": "^3.6.14", "react": "^16.8.6", "react-dom": "^16.8.6", "react-feather": "^2.0.3" diff --git a/src/common/peer.js b/src/common/peer.js new file mode 100644 index 0000000..a84d771 --- /dev/null +++ b/src/common/peer.js @@ -0,0 +1,172 @@ +import IPFS from 'ipfs' +import Libp2p from 'libp2p' +import TCP from 'libp2p-tcp' +import Multiplex from 'libp2p-mplex' +import SECIO from 'libp2p-secio' +import defaultsDeep from '@nodeutils/defaults-deep' +import KadDHT from 'libp2p-kad-dht' +import Ping from 'libp2p/src/ping' +// import WebRTCStar from 'libp2p-webrtc-star' +import multiaddr from 'multiaddr' +import PeerInfo from 'peer-info' +import PeerId from 'peer-id' + +import store from './store' + +// const wstar = new WebRTCStar() + +const DEFAULT_OPTS = { + modules: { + transport: [TCP], + // discovery: [wstar.discovery], + connEncryption: [SECIO], + streamMuxer: [Multiplex], + dht: KadDHT + }, + config: { + dht: { + enabled: true, + kBucketSize: 20 + } + } +} + +export class P2PNode extends Libp2p { + constructor(opts) { + super(defaultsDeep(opts, DEFAULT_OPTS)) + } + + ping(remotePeerInfo, callback) { + const p = new Ping(this._switch, remotePeerInfo) + p.on('ping', time => { + p.stop() // stop sending pings + callback(null, time) + }) + p.on('error', callback) + p.start() + } + + pingRemotePeer(remoteAddress) { + const remoteAddr = multiaddr(remoteAddress) + + // Convert the multiaddress into a PeerInfo object + const peerId = PeerId.createFromB58String(remoteAddr.getPeerId()) + const remotePeerInfo = new PeerInfo(peerId) + remotePeerInfo.multiaddrs.add(remoteAddr) + + console.log('pinging remote peer at ', remoteAddr.toString()) + this.ping(remotePeerInfo, (err, time) => { + if (err) { + return console.error('error pinging: ', err) + } + console.log(`pinged ${remoteAddr.toString()} in ${time}ms`) + }) + } + + // query dht and get current address + async getAddrFromId(multihash) { + const peerId = PeerId.createFromB58String(multihash) + const result = await this.peerRouting.findPeer(peerId) + console.log({ result }) + return result + } +} + +export const createPeer = async (opts = {}) => { + const createPeerInfo = () => { + return new Promise((resolve, reject) => { + let PeerId = store.get('PeerId') + if (PeerId) { + PeerInfo.create(PeerId, (err, peerInfo) => { + if (err) { + reject(err) + } + resolve(peerInfo) + }) + } else { + PeerInfo.create((err, peerInfo) => { + if (err) { + reject(err) + } + store.set('PeerId', peerInfo.id) + resolve(peerInfo) + }) + } + }) + } + + const peerInfo = await createPeerInfo() + // add a listen address to accept TCP connections on a random port + const listenAddress = multiaddr(`/ip4/127.0.0.1/tcp/0`) + peerInfo.multiaddrs.add(listenAddress) + + console.log({ opts }) + const peer = new P2PNode({ peerInfo, ...opts }) + + // register an event handler for errors. + // here we're just going to print and re-throw the error + // to kill the program + peer.on('error', err => { + console.error('libp2p error: ', err) + throw err + }) + + peer.start(err => { + if (err) { + throw err + } + const addresses = peer.peerInfo.multiaddrs.toArray() + // pingRemotePeer(peer) + console.log('peer started. listening on addresses:') + addresses.forEach(addr => console.log(addr.toString())) + }) + return peer +} + +export const createIPFS = async () => { + const node = await IPFS.create({ + libp2p: { + config: { + dht: { + enabled: true + } + } + } + }) + + // Lets log out the number of peers we have every 2 seconds + setInterval(async () => { + try { + const peers = await node.swarm.peers() + console.log(`The node now has ${peers.length} peers.`) + } catch (err) { + console.log('An error occurred trying to check our peers:', err) + } + }, 2000) + + // Log out the bandwidth stats every 4 seconds so we can see how our configuration is doing + setInterval(async () => { + try { + const stats = await node.stats.bw() + console.log(`\nBandwidth Stats: ${JSON.stringify(stats, null, 2)}\n`) + } catch (err) { + console.log('An error occurred trying to check our stats:', err) + } + }, 4000) + + let peers + peers = await node.swarm.peers() // empty peers (still connecting) + + setTimeout(async () => { + peers = await node.swarm.peers() // several peers (connected now) + + if (peers.length) { + const b58peerId = peers[0].peer._idB58String + const peerId = await node.dht.findPeer(b58peerId) + + console.log('peerId', peerId) + } + }, 5000) + + return node +} diff --git a/src/common/store.js b/src/common/store.js index bfbe6b1..72ddfda 100644 --- a/src/common/store.js +++ b/src/common/store.js @@ -1,4 +1,3 @@ -import electron from 'electron' import Store from 'electron-store' const config = { diff --git a/src/ui/components/App/style.css b/src/ui/app.css similarity index 100% rename from src/ui/components/App/style.css rename to src/ui/app.css diff --git a/src/ui/app.js b/src/ui/app.js new file mode 100644 index 0000000..d598319 --- /dev/null +++ b/src/ui/app.js @@ -0,0 +1,28 @@ +import React from 'react' +import { ApolloProvider } from '@apollo/react-hooks' + +import { client } from './common/apollo' +import { + HashProvider, + PageProvider, + IpfsProvider, + SideMenu +} from './components' +import { Page } from './pages' + +import css from './app.css' + +export const App = () => ( + + + + +
+ + +
+
+
+
+
+) diff --git a/src/ui/components/App/index.js b/src/ui/components/App/index.js deleted file mode 100644 index 66c8089..0000000 --- a/src/ui/components/App/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import { ApolloProvider } from '@apollo/react-hooks' - -import { client } from '../../common/apollo' -import { HashProvider } from '../Context/hash' -import { PageProvider } from '../Context/page' -import { Page } from '../Page' -import { SideMenu } from '../Menu/side' - -import css from './style.css' - -export const App = () => ( - - - -
- - -
-
-
-
-) diff --git a/src/ui/components/Bar/hash.js b/src/ui/components/Bar/index.js similarity index 95% rename from src/ui/components/Bar/hash.js rename to src/ui/components/Bar/index.js index 00cc4a8..746712f 100644 --- a/src/ui/components/Bar/hash.js +++ b/src/ui/components/Bar/index.js @@ -6,9 +6,9 @@ import { Box, Radio } from 'react-feather' import { ipfs } from '../../../common/ipfs' import { HashContext } from '../Context/hash' import useInterval from '../Hooks/useInterval' -import { PinButton } from '../Button/pin' +import { PinButton } from '../Button' -import css from './hash.css' +import css from './style.css' export const HashBar = () => { const { hash, setHash } = useContext(HashContext) diff --git a/src/ui/components/Bar/hash.css b/src/ui/components/Bar/style.css similarity index 100% rename from src/ui/components/Bar/hash.css rename to src/ui/components/Bar/style.css diff --git a/src/ui/components/Button/pin.js b/src/ui/components/Button/index.js similarity index 98% rename from src/ui/components/Button/pin.js rename to src/ui/components/Button/index.js index 1f52c79..786076b 100644 --- a/src/ui/components/Button/pin.js +++ b/src/ui/components/Button/index.js @@ -5,7 +5,7 @@ import { CheckCircle, Download, Loader } from 'react-feather' import { ipfs } from '../../../common/ipfs' import { HashContext } from '../Context/hash' -import css from './pin.css' +import css from './style.css' export const PinButton = () => { const { hash, setHash } = useContext(HashContext) diff --git a/src/ui/components/Button/pin.css b/src/ui/components/Button/style.css similarity index 100% rename from src/ui/components/Button/pin.css rename to src/ui/components/Button/style.css diff --git a/src/ui/components/Context/index.js b/src/ui/components/Context/index.js new file mode 100644 index 0000000..db7e4c3 --- /dev/null +++ b/src/ui/components/Context/index.js @@ -0,0 +1,3 @@ +export { HashProvider, HashContext } from './hash' +export { PageProvider, PageContext } from './page' +export { IpfsProvider, IpfsContext } from './ipfs' diff --git a/src/ui/components/Context/ipfs.js b/src/ui/components/Context/ipfs.js new file mode 100644 index 0000000..496e1da --- /dev/null +++ b/src/ui/components/Context/ipfs.js @@ -0,0 +1,33 @@ +import IPFS from 'ipfs' +import React, { createContext, useState, useEffect } from 'react' + +export const IpfsContext = createContext({ + ipfsNode: null +}) + +export const IpfsConsumer = IpfsContext.Consumer + +export const IpfsProvider = ({ children }) => { + const [ipfsNode, setIpfsNode] = useState(null) + + useEffect(() => { + IPFS.create({ + libp2p: { + config: { + dht: { + enabled: true + } + } + } + }).then((node, error) => { + if (error) { + throw error + } + setIpfsNode(node) + }) + }, []) + + return ( + {children} + ) +} diff --git a/src/ui/components/Hooks/index.js b/src/ui/components/Hooks/index.js new file mode 100644 index 0000000..829c83f --- /dev/null +++ b/src/ui/components/Hooks/index.js @@ -0,0 +1 @@ +export { default as useInterval } from './useInterval' diff --git a/src/ui/components/Spinner/index.js b/src/ui/components/Loaders/Spinner.js similarity index 100% rename from src/ui/components/Spinner/index.js rename to src/ui/components/Loaders/Spinner.js diff --git a/src/ui/components/Loaders/index.js b/src/ui/components/Loaders/index.js new file mode 100644 index 0000000..fbf16c1 --- /dev/null +++ b/src/ui/components/Loaders/index.js @@ -0,0 +1 @@ +export { Spinner } from './Spinner' diff --git a/src/ui/components/Spinner/style.css b/src/ui/components/Loaders/style.css similarity index 100% rename from src/ui/components/Spinner/style.css rename to src/ui/components/Loaders/style.css diff --git a/src/ui/components/Menu/side.js b/src/ui/components/Menu/index.js similarity index 97% rename from src/ui/components/Menu/side.js rename to src/ui/components/Menu/index.js index 7cd86fc..0d837ed 100644 --- a/src/ui/components/Menu/side.js +++ b/src/ui/components/Menu/index.js @@ -3,7 +3,7 @@ import { FileText, GitCommit, Radio, Search } from 'react-feather' import { PageContext } from '../Context/page' -import css from './side.css' +import css from './style.css' export const SideMenu = () => { const { page, setPage } = useContext(PageContext) diff --git a/src/ui/components/Menu/side.css b/src/ui/components/Menu/style.css similarity index 100% rename from src/ui/components/Menu/side.css rename to src/ui/components/Menu/style.css diff --git a/src/ui/components/index.js b/src/ui/components/index.js new file mode 100644 index 0000000..34978e5 --- /dev/null +++ b/src/ui/components/index.js @@ -0,0 +1,7 @@ +export * from './Bar' +export * from './Button' +export * from './Context' +export * from './Hooks' +export * from './Loaders' +export * from './Menu' +export * from './Welcome' diff --git a/src/ui/index.js b/src/ui/index.js index 2493c3c..4c34156 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -1,6 +1,6 @@ import React from 'react' import { render } from 'react-dom' -import { App } from './components/App' +import { App } from './app' render(, document.getElementById('root')) diff --git a/src/ui/pages/Peers/chatRoom.js b/src/ui/pages/Peers/chatRoom.js new file mode 100644 index 0000000..029ba86 --- /dev/null +++ b/src/ui/pages/Peers/chatRoom.js @@ -0,0 +1,139 @@ +import React, { useEffect, useState, useContext } from 'react' +import PeerId from 'peer-id' +import pull from 'pull-stream' + +import { IpfsContext } from '../../components' + +const protocalName = '/chat/1.0.0' + +export const ChatRoom = () => { + const { ipfsNode } = useContext(IpfsContext) + + const [viewerId, setViewerId] = useState('') + const [remotePeerId, setRemotePeerId] = useState('') + const [remotePeerInfo, setRemotePeerInfo] = useState('') + + const [messageDraft, setMessageDraft] = useState('') + const [dialog, setDialog] = useState([]) + + // get viewer id, assign protocal + useEffect(() => { + if (ipfsNode) { + ipfsNode.id().then(({ id }, err) => { + if (err) { + throw err + } + setViewerId(id) + }) + + ipfsNode.libp2p.handle(protocalName, (protocol, connection) => { + pull( + connection, + pull.collect((err, data) => { + if (err) { + throw err + } + + const message = JSON.parse(data.toString()) + setDialog(dialog => dialog.concat([message])) + + // temperally set repose target for now + setRemotePeerId(message.from) + }) + ) + }) + } + }) + + // get address from id + useEffect(() => { + if (ipfsNode && remotePeerId) { + const peerId = PeerId.createFromB58String(remotePeerId) + ipfsNode.libp2p.peerRouting.findPeer(peerId).then((peerInfo, err) => { + if (err) { + throw err + } + setRemotePeerInfo(peerInfo) + }) + } + }, [remotePeerId]) + + const handleSubmit = event => { + if (ipfsNode && remotePeerInfo) { + const message = { content: messageDraft, from: viewerId } + ipfsNode.libp2p.dialProtocol( + remotePeerInfo, + protocalName, + (err, connection) => { + pull(pull.values([JSON.stringify(message)]), connection) + } + ) + setDialog(dialog => dialog.concat([message])) + } else { + console.error('connection not established', messageDraft) + } + event.preventDefault() + } + + return ( +
+ My id: {viewerId} +
+ { + setRemotePeerId(e.target.value) + }} + style={{ margin: 10 }} + /> +
+ +
+ {dialog.map(({ content, from }, index) => ( + + {content} + + ))} +
+
+ { + setMessageDraft(e.target.value) + }} + style={{ margin: 10, width: '60%' }} + /> +
+
+ ) +} diff --git a/src/ui/components/Page/peers.css b/src/ui/pages/Peers/index.css similarity index 90% rename from src/ui/components/Page/peers.css rename to src/ui/pages/Peers/index.css index 298db76..cfeb1e2 100644 --- a/src/ui/components/Page/peers.css +++ b/src/ui/pages/Peers/index.css @@ -9,7 +9,7 @@ flex-direction: column; border-bottom: 1px solid rgba(239, 232, 227, 0.6); - color: #725B54; + color: #725b54; font-size: 0.9rem; width: 100%; padding: 2rem 0 1rem 2rem; @@ -21,12 +21,12 @@ } .idIcon { - color: #725B54; + color: #725b54; margin: 0 0.5rem 0 0; } .idText { - color: #D86143; + color: #d86143; margin: 0 0 0 0.3rem; } @@ -37,12 +37,12 @@ } .peersIcon { - color: #725B54; + color: #725b54; margin: 0 0.5rem 0 0; } .peersText { - color: #D86143; + color: #d86143; margin: 0 0 0 0.3rem; } @@ -54,12 +54,12 @@ display: flex; align-items: flex-start; flex-direction: column; - + height: 30%; padding: 2rem 1rem 1rem 2rem; } .head { - color: #725B54; + color: #725b54; font-size: 0.9rem; font-weight: bold; width: 100%; diff --git a/src/ui/components/Page/peers.js b/src/ui/pages/Peers/index.js similarity index 90% rename from src/ui/components/Page/peers.js rename to src/ui/pages/Peers/index.js index cb0f597..1cde69e 100644 --- a/src/ui/components/Page/peers.js +++ b/src/ui/pages/Peers/index.js @@ -2,10 +2,11 @@ import React, { useEffect, useState } from 'react' import { Radio, User } from 'react-feather' import { ipfs } from '../../../common/ipfs' -import useInterval from '../Hooks/useInterval' -import { Spinner } from '../Spinner' +import useInterval from '../../components/Hooks/useInterval' +import { Spinner } from '../../components/Loaders/Spinner' -import css from './peers.css' +import css from './index.css' +import { ChatRoom } from './chatRoom' const PeersTable = ({ peers }) => { const rows = peers.map((peer, index) => ( @@ -84,6 +85,8 @@ export const Peers = () => { /> )} {!loading && } + + ) } diff --git a/src/ui/components/Page/articles.css b/src/ui/pages/articles.css similarity index 100% rename from src/ui/components/Page/articles.css rename to src/ui/pages/articles.css diff --git a/src/ui/components/Page/articles.js b/src/ui/pages/articles.js similarity index 94% rename from src/ui/components/Page/articles.js rename to src/ui/pages/articles.js index 096015e..3f9150c 100644 --- a/src/ui/components/Page/articles.js +++ b/src/ui/pages/articles.js @@ -2,10 +2,8 @@ import React, { useContext } from 'react' import { useQuery } from '@apollo/react-hooks' import gql from 'graphql-tag' -import { Spinner } from '../Spinner' +import { Spinner, HashContext, PageContext } from '../components' import css from './articles.css' -import { HashContext } from '../Context/hash' -import { PageContext } from '../Context/page' export const Articles = () => { const SUBSCRIPTIONS = gql` diff --git a/src/ui/components/Page/bootstrap.css b/src/ui/pages/bootstrap.css similarity index 100% rename from src/ui/components/Page/bootstrap.css rename to src/ui/pages/bootstrap.css diff --git a/src/ui/components/Page/bootstrap.js b/src/ui/pages/bootstrap.js similarity index 96% rename from src/ui/components/Page/bootstrap.js rename to src/ui/pages/bootstrap.js index 7c348ad..058d455 100644 --- a/src/ui/components/Page/bootstrap.js +++ b/src/ui/pages/bootstrap.js @@ -2,8 +2,8 @@ import trim from 'lodash/trim' import React, { useEffect, useState } from 'react' import { GitCommit, Trash2, User } from 'react-feather' -import { ipfs } from '../../../common/ipfs' -import { Spinner } from '../Spinner' +import { ipfs } from '../../common/ipfs' +import { Spinner } from '../components/Loaders/Spinner' import css from './bootstrap.css' diff --git a/src/ui/components/Page/explore.css b/src/ui/pages/explore.css similarity index 100% rename from src/ui/components/Page/explore.css rename to src/ui/pages/explore.css diff --git a/src/ui/components/Page/explore.js b/src/ui/pages/explore.js similarity index 84% rename from src/ui/components/Page/explore.js rename to src/ui/pages/explore.js index be41dba..684722e 100644 --- a/src/ui/components/Page/explore.js +++ b/src/ui/pages/explore.js @@ -1,10 +1,7 @@ import React, { useContext, useEffect, useState, useRef } from 'react' -import { getLocalHttp } from '../../../common/ipfs' -import { HashContext } from '../Context/hash' -import { HashBar } from '../Bar/hash' -import { Welcome } from '../Welcome' -import { Spinner } from '../Spinner' +import { getLocalHttp } from '../../common/ipfs' +import { Welcome, Spinner, HashBar, HashContext } from '../components' import css from './explore.css' diff --git a/src/ui/components/Page/index.js b/src/ui/pages/index.js similarity index 85% rename from src/ui/components/Page/index.js rename to src/ui/pages/index.js index ce6e2bf..fd1bb7f 100644 --- a/src/ui/components/Page/index.js +++ b/src/ui/pages/index.js @@ -1,10 +1,10 @@ import React, { useContext } from 'react' -import { PageContext } from '../Context/page' +import { PageContext } from '../components' import { Articles } from './articles' import { Bootstrap } from './bootstrap' import { Explore } from './explore' -import { Peers } from './peers' +import { Peers } from './Peers' export const Page = () => { const { page } = useContext(PageContext)