Import browser chat
This commit is contained in:
parent
f6f5d9b01e
commit
7bdd23bbce
|
@ -4,3 +4,4 @@
|
|||
tmp/
|
||||
|
||||
node_modules
|
||||
.cache
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr" class="h-100">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Waku Web chat</title>
|
||||
</head>
|
||||
<body class="h-100">
|
||||
<div id="root" class="h-100"></div>
|
||||
<script src="./index.js" ></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,7 @@
|
|||
import 'regenerator-runtime/runtime.js'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './views/App'
|
||||
import createLibp2p from './libp2p'
|
||||
|
||||
ReactDOM.render(<App createLibp2p={createLibp2p} />, document.getElementById('root'))
|
|
@ -0,0 +1,56 @@
|
|||
// Libp2p Core
|
||||
import Libp2p from 'libp2p'
|
||||
// Transports
|
||||
import Websockets from 'libp2p-websockets'
|
||||
import WebrtcStar from 'libp2p-webrtc-star'
|
||||
// Stream Muxer
|
||||
import Mplex from 'libp2p-mplex'
|
||||
// Connection Encryption
|
||||
import { NOISE } from 'libp2p-noise'
|
||||
import Secio from 'libp2p-secio'
|
||||
// Peer Discovery
|
||||
import Bootstrap from 'libp2p-bootstrap'
|
||||
import KadDHT from 'libp2p-kad-dht'
|
||||
// Gossipsub
|
||||
import Gossipsub from 'libp2p-gossipsub'
|
||||
|
||||
const createLibp2p = async (peerId) => {
|
||||
// Create the Node
|
||||
const libp2p = await Libp2p.create({
|
||||
peerId,
|
||||
addresses: {
|
||||
listen: [
|
||||
// Add the signaling server multiaddr
|
||||
'/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star'
|
||||
]
|
||||
},
|
||||
modules: {
|
||||
transport: [Websockets, WebrtcStar],
|
||||
streamMuxer: [Mplex],
|
||||
connEncryption: [NOISE, Secio],
|
||||
peerDiscovery: [Bootstrap],
|
||||
dht: KadDHT,
|
||||
pubsub: Gossipsub
|
||||
},
|
||||
config: {
|
||||
peerDiscovery: {
|
||||
bootstrap: {
|
||||
list: ['/ip4/127.0.0.1/tcp/63786/ws/p2p/QmWjz6xb8v9K4KnYEwP5Yk75k5mMBCehzWFLCvvQpYxF3d']
|
||||
}
|
||||
},
|
||||
dht: {
|
||||
enabled: true,
|
||||
randomWalk: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Automatically start libp2p
|
||||
await libp2p.start()
|
||||
|
||||
return libp2p
|
||||
}
|
||||
|
||||
export default createLibp2p
|
|
@ -0,0 +1,191 @@
|
|||
const protons = require('protons')
|
||||
const EventEmitter = require('events')
|
||||
|
||||
const { Request, Stats } = protons(`
|
||||
message Request {
|
||||
enum Type {
|
||||
SEND_MESSAGE = 0;
|
||||
UPDATE_PEER = 1;
|
||||
STATS = 2;
|
||||
}
|
||||
|
||||
required Type type = 1;
|
||||
optional SendMessage sendMessage = 2;
|
||||
optional UpdatePeer updatePeer = 3;
|
||||
optional Stats stats = 4;
|
||||
}
|
||||
|
||||
message SendMessage {
|
||||
required bytes data = 1;
|
||||
required int64 created = 2;
|
||||
required bytes id = 3;
|
||||
}
|
||||
|
||||
message UpdatePeer {
|
||||
optional bytes userHandle = 1;
|
||||
}
|
||||
|
||||
message Stats {
|
||||
enum NodeType {
|
||||
GO = 0;
|
||||
NODEJS = 1;
|
||||
BROWSER = 2;
|
||||
}
|
||||
|
||||
repeated bytes connectedPeers = 1;
|
||||
optional NodeType nodeType = 2;
|
||||
}
|
||||
`)
|
||||
|
||||
class Chat extends EventEmitter {
|
||||
/**
|
||||
*
|
||||
* @param {Libp2p} libp2p A Libp2p node to communicate through
|
||||
* @param {string} topic The topic to subscribe to
|
||||
*/
|
||||
constructor (libp2p, topic) {
|
||||
super()
|
||||
|
||||
this.libp2p = libp2p
|
||||
this.topic = topic
|
||||
|
||||
this.connectedPeers = new Set()
|
||||
this.stats = new Map()
|
||||
this.libp2p.connectionManager.on('peer:connect', (connection) => {
|
||||
console.log('Connected to', connection.remotePeer.toB58String())
|
||||
if (this.connectedPeers.has(connection.remotePeer.toB58String())) return
|
||||
this.connectedPeers.add(connection.remotePeer.toB58String())
|
||||
this.sendStats(Array.from(this.connectedPeers))
|
||||
})
|
||||
this.libp2p.connectionManager.on('peer:disconnect', (connection) => {
|
||||
console.log('Disconnected from', connection.remotePeer.toB58String())
|
||||
if (this.connectedPeers.delete(connection.remotePeer.toB58String())) {
|
||||
this.sendStats(Array.from(this.connectedPeers))
|
||||
}
|
||||
})
|
||||
|
||||
// Join if libp2p is already on
|
||||
if (this.libp2p.isStarted()) this.join()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to `Chat.topic`. All messages will be
|
||||
* forwarded to `messageHandler`
|
||||
* @private
|
||||
*/
|
||||
join () {
|
||||
this.libp2p.pubsub.subscribe(this.topic, (message) => {
|
||||
try {
|
||||
const request = Request.decode(message.data)
|
||||
switch (request.type) {
|
||||
case Request.Type.UPDATE_PEER:
|
||||
this.emit('peer:update', {
|
||||
id: message.from,
|
||||
name: request.updatePeer.userHandle.toString()
|
||||
})
|
||||
break
|
||||
case Request.Type.STATS:
|
||||
this.stats.set(message.from, request.stats)
|
||||
console.log('Incoming Stats:', message.from, request.stats)
|
||||
this.emit('stats', this.stats)
|
||||
break
|
||||
default:
|
||||
this.emit('message', {
|
||||
from: message.from,
|
||||
...request.sendMessage
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from `Chat.topic`
|
||||
* @private
|
||||
*/
|
||||
leave () {
|
||||
this.libp2p.pubsub.unsubscribe(this.topic)
|
||||
}
|
||||
|
||||
/**
|
||||
* Crudely checks the input for a command. If no command is
|
||||
* found `false` is returned. If the input contains a command,
|
||||
* that command will be processed and `true` will be returned.
|
||||
* @param {Buffer|string} input Text submitted by the user
|
||||
* @returns {boolean} Whether or not there was a command
|
||||
*/
|
||||
checkCommand (input) {
|
||||
const str = input.toString()
|
||||
if (str.startsWith('/')) {
|
||||
const args = str.slice(1).split(' ')
|
||||
switch (args[0]) {
|
||||
case 'name':
|
||||
this.updatePeer(args[1])
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message over pubsub to update the user handle
|
||||
* to the provided `name`.
|
||||
* @param {Buffer|string} name Username to change to
|
||||
*/
|
||||
async updatePeer (name) {
|
||||
const msg = Request.encode({
|
||||
type: Request.Type.UPDATE_PEER,
|
||||
updatePeer: {
|
||||
userHandle: Buffer.from(name)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await this.libp2p.pubsub.publish(this.topic, msg)
|
||||
} catch (err) {
|
||||
console.error('Could not publish name change')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the updated stats to the pubsub network
|
||||
* @param {Array<Buffer>} connectedPeers
|
||||
*/
|
||||
async sendStats (connectedPeers) {
|
||||
const msg = Request.encode({
|
||||
type: Request.Type.STATS,
|
||||
stats: {
|
||||
connectedPeers,
|
||||
nodeType: Stats.NodeType.BROWSER
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await this.libp2p.pubsub.publish(this.topic, msg)
|
||||
} catch (err) {
|
||||
console.error('Could not publish stats update')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the given `message` to pubsub peers
|
||||
* @param {Buffer|string} message The chat message to send
|
||||
*/
|
||||
async send (message) {
|
||||
const msg = Request.encode({
|
||||
type: Request.Type.SEND_MESSAGE,
|
||||
sendMessage: {
|
||||
id: (~~(Math.random() * 1e9)).toString(36) + Date.now(),
|
||||
data: Buffer.from(message),
|
||||
created: Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
await this.libp2p.pubsub.publish(this.topic, msg)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Chat
|
||||
module.exports.TOPIC = '/libp2p/example/chat/1.0.0'
|
|
@ -0,0 +1,18 @@
|
|||
import PeerId from 'peer-id'
|
||||
|
||||
export async function getOrCreatePeerId () {
|
||||
let peerId
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
peerId = JSON.parse(localStorage.getItem('peerId'))
|
||||
peerId = await PeerId.createFromJSON(peerId)
|
||||
} catch (err) {
|
||||
console.info('Could not get the stored peer id, a new one will be generated', err)
|
||||
peerId = await PeerId.create()
|
||||
console.info('Storing our peer id in local storage so it can be reused')
|
||||
// eslint-disable-next-line
|
||||
localStorage.setItem('peerId', JSON.stringify(peerId.toJSON()))
|
||||
}
|
||||
|
||||
return peerId
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
'use strict'
|
||||
|
||||
module.exports.layout = {
|
||||
name: 'euler',
|
||||
|
||||
// The ideal length of a spring
|
||||
// - This acts as a hint for the edge length
|
||||
// - The edge length can be longer or shorter if the forces are set to extreme values
|
||||
springLength: edge => 80,
|
||||
|
||||
// Hooke's law coefficient
|
||||
// - The value ranges on [0, 1]
|
||||
// - Lower values give looser springs
|
||||
// - Higher values give tighter springs
|
||||
springCoeff: edge => 0.0008,
|
||||
|
||||
// The mass of the node in the physics simulation
|
||||
// - The mass affects the gravity node repulsion/attraction
|
||||
mass: node => 4,
|
||||
|
||||
// Coulomb's law coefficient
|
||||
// - Makes the nodes repel each other for negative values
|
||||
// - Makes the nodes attract each other for positive values
|
||||
gravity: -1.2,
|
||||
|
||||
// A force that pulls nodes towards the origin (0, 0)
|
||||
// Higher values keep the components less spread out
|
||||
pull: 0.001,
|
||||
|
||||
// Theta coefficient from Barnes-Hut simulation
|
||||
// - Value ranges on [0, 1]
|
||||
// - Performance is better with smaller values
|
||||
// - Very small values may not create enough force to give a good result
|
||||
theta: 0.666,
|
||||
|
||||
// Friction / drag coefficient to make the system stabilise over time
|
||||
dragCoeff: 0.02,
|
||||
|
||||
// When the total of the squared position deltas is less than this value, the simulation ends
|
||||
movementThreshold: 1,
|
||||
|
||||
// The amount of time passed per tick
|
||||
// - Larger values result in faster runtimes but might spread things out too far
|
||||
// - Smaller values produce more accurate results
|
||||
timeStep: 20,
|
||||
|
||||
// The number of ticks per frame for animate:true
|
||||
// - A larger value reduces rendering cost but can be jerky
|
||||
// - A smaller value increases rendering cost but is smoother
|
||||
refresh: 10,
|
||||
|
||||
// Whether to animate the layout
|
||||
// - true : Animate while the layout is running
|
||||
// - false : Just show the end result
|
||||
// - 'end' : Animate directly to the end result
|
||||
animate: true,
|
||||
|
||||
// Animation duration used for animate:'end'
|
||||
animationDuration: undefined,
|
||||
|
||||
// Easing for animate:'end'
|
||||
animationEasing: undefined,
|
||||
|
||||
// Maximum iterations and time (in ms) before the layout will bail out
|
||||
// - A large value may allow for a better result
|
||||
// - A small value may make the layout end prematurely
|
||||
// - The layout may stop before this if it has settled
|
||||
maxIterations: 1000,
|
||||
maxSimulationTime: 4000,
|
||||
|
||||
// Prevent the user grabbing nodes during the layout (usually with animate:true)
|
||||
ungrabifyWhileSimulating: false,
|
||||
|
||||
// Whether to fit the viewport to the repositioned graph
|
||||
// true : Fits at end of layout for animate:false or animate:'end'; fits on each frame for animate:true
|
||||
fit: true,
|
||||
|
||||
// Padding in rendered co-ordinates around the layout
|
||||
padding: 30,
|
||||
|
||||
// Constrain layout bounds with one of
|
||||
// - { x1, y1, x2, y2 }
|
||||
// - { x1, y1, w, h }
|
||||
// - undefined / null : Unconstrained
|
||||
boundingBox: undefined,
|
||||
|
||||
// Layout event callbacks; equivalent to `layout.one('layoutready', callback)` for example
|
||||
ready: function () {}, // on layoutready
|
||||
stop: function () {}, // on layoutstop
|
||||
|
||||
// Whether to randomize the initial positions of the nodes
|
||||
// true : Use random positions within the bounding box
|
||||
// false : Use the current node positions as the initial positions
|
||||
randomize: false
|
||||
}
|
||||
|
||||
module.exports.style = [{
|
||||
selector: '.node-type-me', // ME
|
||||
style: {
|
||||
'background-color': '#791B89'
|
||||
}
|
||||
}, {
|
||||
selector: '.node-type-2', // BROWSER
|
||||
style: {
|
||||
'background-color': '#E88900'
|
||||
}
|
||||
}, {
|
||||
selector: '.node-type-1', // NODEJS
|
||||
style: {
|
||||
'background-color': '#A3AA12'
|
||||
}
|
||||
}, {
|
||||
selector: '.node-type-0', // GO
|
||||
style: {
|
||||
'background-color': '#22616A'
|
||||
}
|
||||
}, {
|
||||
selector: 'edge',
|
||||
style: {
|
||||
width: 1,
|
||||
'line-color': '#ccc',
|
||||
'target-arrow-color': '#ccc'
|
||||
}
|
||||
}]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "js-webapp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "parcel $INIT_CWD/index.html -d ./dist",
|
||||
"lint": "standard"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cytoscape": "^3.15.0",
|
||||
"cytoscape-euler": "^1.2.2",
|
||||
"it-pipe": "^1.1.0",
|
||||
"libp2p": "^0.28.0",
|
||||
"libp2p-bootstrap": "^0.11.0",
|
||||
"libp2p-floodsub": "^0.21.3",
|
||||
"libp2p-gossipsub": "^0.4.5",
|
||||
"libp2p-kad-dht": "^0.19.5",
|
||||
"libp2p-mplex": "^0.9.5",
|
||||
"libp2p-noise": "^1.1.1",
|
||||
"libp2p-secio": "^0.12.5",
|
||||
"libp2p-webrtc-star": "^0.18.3",
|
||||
"libp2p-websockets": "^0.13.6",
|
||||
"peer-id": "^0.13.12",
|
||||
"protons": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"parcel": "^1.12.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"standard": "^14.3.4",
|
||||
"tachyons": "^4.12.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Chrome versions"
|
||||
],
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,60 @@
|
|||
@import 'tachyons';
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nexa';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('fonts/Nexa_Free_Light.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nexa';
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
src: url('fonts/Nexa_Free_Bold.otf') format('opentype');
|
||||
}
|
||||
|
||||
.nexa { font-family: 'Nexa', 'Verdana', system-ui, sans-serif; }
|
||||
.bg-dark-blue { background-color: #14162D; }
|
||||
|
||||
|
||||
.chat-header {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.chat-body {
|
||||
margin: 1em 0;
|
||||
}
|
||||
.chat-body p {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
.chat-time {
|
||||
float: right;
|
||||
}
|
||||
.right { text-align: right; }
|
||||
.right .chat-time {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.dot {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
background-color: #999;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bg-libp2p-dark-aqua {
|
||||
background-color: #22616A;
|
||||
}
|
||||
|
||||
.bg-libp2p-dark-purple {
|
||||
background-color: #791B89;
|
||||
}
|
||||
|
||||
.bg-libp2p-dark-orange {
|
||||
background-color: #E88900;
|
||||
}
|
||||
|
||||
.bg-libp2p-dark-green {
|
||||
background-color: #A3AA12;
|
||||
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,52 @@
|
|||
/* eslint-env browser */
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import EventEmitter from 'events'
|
||||
import Header from '../views/Header'
|
||||
import Metrics from './Metrics'
|
||||
import Chat from './Chat'
|
||||
import { getOrCreatePeerId } from '../libs/peer-id'
|
||||
import '../styles/index.css'
|
||||
|
||||
export default function App ({
|
||||
createLibp2p
|
||||
}) {
|
||||
const [peerId, setPeerId] = useState(null)
|
||||
const [libp2p, setLibp2p] = useState(null)
|
||||
const [started, setStarted] = useState(false)
|
||||
const eventBus = new EventEmitter()
|
||||
|
||||
/**
|
||||
* Leverage use effect to act on state changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
// If we don't have a PeerId, let's get or create it
|
||||
// This will attempt to leverage localStorage so we can reuse our key
|
||||
if (!peerId) {
|
||||
console.info('Getting our PeerId')
|
||||
getOrCreatePeerId().then(setPeerId)
|
||||
return
|
||||
}
|
||||
|
||||
// If the libp2p instance is not created, create it with our PeerId instance
|
||||
if (!libp2p) {
|
||||
;(async () => {
|
||||
console.info('Creating our Libp2p instance')
|
||||
const node = await createLibp2p(peerId)
|
||||
setLibp2p(node)
|
||||
setStarted(true)
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='avenir flex flex-column h-100'>
|
||||
<div className='flex-none'>
|
||||
<Header started={started} />
|
||||
</div>
|
||||
<div className='flex h-100'>
|
||||
<Metrics libp2p={libp2p} eventBus={eventBus} />
|
||||
<Chat libp2p={libp2p} eventBus={eventBus} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import Message from './Message'
|
||||
|
||||
// Chat over Pubsub
|
||||
import PubsubChat from '../libs/chat'
|
||||
|
||||
export default function Chat ({
|
||||
libp2p,
|
||||
eventBus
|
||||
}) {
|
||||
const [message, setMessage] = useState('')
|
||||
const [messages, setMessages] = useState([])
|
||||
const [chatClient, setChatClient] = useState(null)
|
||||
const [peers, setPeers] = useState({})
|
||||
|
||||
/**
|
||||
* Sends the current message in the chat field
|
||||
*/
|
||||
const sendMessage = async () => {
|
||||
setMessage('')
|
||||
if (!message) return
|
||||
|
||||
if (chatClient.checkCommand(message)) return
|
||||
|
||||
try {
|
||||
await chatClient.send(message)
|
||||
console.info('Publish done')
|
||||
} catch (err) {
|
||||
console.error('Could not send message', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `sendMessage` if enter was pressed
|
||||
* @param {KeyDownEvent} e
|
||||
*/
|
||||
const onKeyDown = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leverage use effect to act on state changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Wait for libp2p
|
||||
if (!libp2p) return
|
||||
|
||||
// Create the pubsub chatClient
|
||||
if (!chatClient) {
|
||||
const pubsubChat = new PubsubChat(libp2p, PubsubChat.TOPIC)
|
||||
|
||||
// Listen for messages
|
||||
pubsubChat.on('message', (message) => {
|
||||
if (message.from === libp2p.peerId.toB58String()) {
|
||||
message.isMine = true
|
||||
}
|
||||
setMessages((messages) => [...messages, message])
|
||||
})
|
||||
// Listen for peer updates
|
||||
pubsubChat.on('peer:update', ({ id, name }) => {
|
||||
setPeers((peers) => {
|
||||
const newPeers = { ...peers }
|
||||
newPeers[id] = { name }
|
||||
return newPeers
|
||||
})
|
||||
})
|
||||
// Forward stats events to the eventBus
|
||||
pubsubChat.on('stats', (stats) => eventBus.emit('stats', stats))
|
||||
|
||||
setChatClient(pubsubChat)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex flex-column w-50 pa3 h-100 bl b--black-10'>
|
||||
<div className='w-100 flex-auto'>
|
||||
<ul className='list pa0'>
|
||||
{messages.map((message, index) => {
|
||||
return <Message peers={peers} message={message} key={message.message ? message.message.id : index} />
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='w-100 h-auto'>
|
||||
<input onChange={e => setMessage(e.target.value)} onKeyDown={(e) => onKeyDown(e)} className='f6 f5-l input-reset fl ba b--black-20 bg-white pa3 lh-solid w-100 w-75-m w-80-l br2-ns br--left-ns' type='text' name='send' value={message} placeholder='Type your message...' />
|
||||
<input onClick={() => sendMessage()} className='f6 f5-l button-reset fl pv3 tc bn bg-animate bg-black-70 hover-bg-black white pointer w-100 w-25-m w-20-l br2-ns br--right-ns' type='submit' value='Send' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import Logo from '../styles/libp2p-logo.svg'
|
||||
|
||||
export default function Header ({
|
||||
started
|
||||
}) {
|
||||
return (
|
||||
<header className='flex items-center pa2 bg-dark-blue'>
|
||||
<a href='https://libp2p.io' title='home' className='w-50'>
|
||||
<img className={started ? 'libp2p-on' : 'libp2p-off'} src={Logo} style={{ height: 50 }} />
|
||||
</a>
|
||||
<h1 className='w-50 ma0 tr f3 fw2 nexa white'><span className='fw7'>libp</span>p2p chat</h1>
|
||||
</header>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Message renderer
|
||||
* @param {object} param0
|
||||
*/
|
||||
export default function Message ({ peers, message }) {
|
||||
const from = peers[message.from] ? peers[message.from].name : message.from.substring(0, 20)
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div className={'chat-body ' + (message.isMine ? 'right' : '')}>
|
||||
<div className='chat-header'>
|
||||
<strong className='chat-name'>{from}</strong>
|
||||
<small className='chat-time'>{new Date(message.created).toLocaleTimeString()}</small>
|
||||
</div>
|
||||
<p>{message.data.toString()}</p>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/* eslint-env browser */
|
||||
import React, { useState, useEffect, createRef } from 'react'
|
||||
import cytoscape from 'cytoscape'
|
||||
import euler from 'cytoscape-euler'
|
||||
import { layout, style } from '../libs/viz'
|
||||
|
||||
function Graph ({
|
||||
graphRoot
|
||||
}) {
|
||||
return (
|
||||
<div ref={graphRoot} className='bg-snow-muted h-100' />
|
||||
)
|
||||
}
|
||||
|
||||
export default function Metrics ({
|
||||
libp2p,
|
||||
eventBus
|
||||
}) {
|
||||
const [listening, setListening] = useState(false)
|
||||
const [peerCount, setPeerCount] = useState(0)
|
||||
const [stats, setStats] = useState(new Map())
|
||||
const _graphRoot = createRef()
|
||||
cytoscape.use(euler)
|
||||
|
||||
/**
|
||||
* Leverage use effect to act on state changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Wait for libp2p
|
||||
if (!libp2p) return
|
||||
|
||||
// Set up listeners for setting metrics
|
||||
if (!listening) {
|
||||
eventBus.on('stats', (stats) => {
|
||||
setStats(stats)
|
||||
})
|
||||
|
||||
libp2p.peerStore.on('peer', (peerId) => {
|
||||
const num = libp2p.peerStore.peers.size
|
||||
setPeerCount(num)
|
||||
})
|
||||
|
||||
setListening(true)
|
||||
return
|
||||
}
|
||||
|
||||
const nodes = {}
|
||||
const edges = []
|
||||
Array.from(stats).forEach(([peerId, stat], index) => {
|
||||
let { connectedPeers, nodeType } = stat
|
||||
if (peerId === libp2p.peerId.toB58String()) {
|
||||
nodeType = 'me'
|
||||
}
|
||||
const classname = `node-type-${nodeType}`
|
||||
|
||||
nodes[peerId] = {
|
||||
data: { id: peerId },
|
||||
classes: classname
|
||||
}
|
||||
|
||||
connectedPeers.forEach(peer => {
|
||||
peer = peer.toString()
|
||||
if (!nodes[peer]) {
|
||||
nodes[peer] = {
|
||||
data: { id: peer }
|
||||
}
|
||||
}
|
||||
|
||||
edges.push({
|
||||
data: {
|
||||
id: `${peerId}-${peer}`,
|
||||
source: peerId,
|
||||
target: peer
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const cy = cytoscape({
|
||||
elements: [
|
||||
...Object.values(nodes),
|
||||
...edges
|
||||
],
|
||||
container: _graphRoot.current,
|
||||
style
|
||||
})
|
||||
cy.layout(layout).run()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex flex-column w-50 pa3 h-100'>
|
||||
<div className='dt dt--fixed w-100'>
|
||||
<p className='dtc'><span className='dot bg-libp2p-dark-purple' /> Me</p>
|
||||
<p className='dtc'><span className='dot bg-libp2p-dark-aqua' /> Go</p>
|
||||
<p className='dtc'><span className='dot bg-libp2p-dark-orange' /> Browser</p>
|
||||
<p className='dtc'><span className='dot bg-libp2p-dark-green' /> Node.js</p>
|
||||
<p className='dtc'><span className='dot' /> Unknown</p>
|
||||
</div>
|
||||
<h3>Peers Known: {peerCount}</h3>
|
||||
<Graph graphRoot={_graphRoot} />
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue