mirror of
https://github.com/vacp2p/universal-connectivity.git
synced 2026-01-10 07:38:15 -05:00
JS peer improvements (#116)
* feat: delegated routing for bootstrap multiaddrs * feat: allow getting messages when not on chat page also clean up and refactor some of the logic * docs: add log line * chore: disable eslint warning * chore: enable indexeddb datastore --------- Co-authored-by: Daniel N <2color@users.noreply.github.com>
This commit is contained in:
100
js-peer/package-lock.json
generated
100
js-peer/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"@chainsafe/libp2p-yamux": "^6.0.2",
|
||||
"@download/blockies": "^1.0.3",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@helia/delegated-routing-v1-http-api-client": "^3.0.1",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@libp2p/bootstrap": "^10.0.20",
|
||||
"@libp2p/circuit-relay-v2": "^1.0.20",
|
||||
@@ -21,6 +22,7 @@
|
||||
"@libp2p/websockets": "^8.0.20",
|
||||
"@libp2p/webtransport": "^4.0.27",
|
||||
"@multiformats/multiaddr": "^12.2.1",
|
||||
"datastore-idb": "^2.1.9",
|
||||
"debug": "^4.3.4",
|
||||
"it-length-prefixed": "^9.0.4",
|
||||
"it-map": "^3.0.5",
|
||||
@@ -2489,6 +2491,32 @@
|
||||
"react-dom": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@helia/delegated-routing-v1-http-api-client": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@helia/delegated-routing-v1-http-api-client/-/delegated-routing-v1-http-api-client-3.0.1.tgz",
|
||||
"integrity": "sha512-Gkaw3B8IjgCCXtasa17j8wQaFdzph2s0RiLyrJTS2htmwVFWZEjLNLUDJeWsMHBoLBza8SrVGvQ9fiN3XD3rfg==",
|
||||
"dependencies": {
|
||||
"@libp2p/interface": "^1.1.1",
|
||||
"@libp2p/logger": "^4.0.4",
|
||||
"@libp2p/peer-id": "^4.0.4",
|
||||
"@multiformats/multiaddr": "^12.1.3",
|
||||
"any-signal": "^4.1.1",
|
||||
"browser-readablestream-to-it": "^2.0.3",
|
||||
"ipns": "^9.0.0",
|
||||
"it-first": "^3.0.3",
|
||||
"it-map": "^3.0.4",
|
||||
"it-ndjson": "^1.0.4",
|
||||
"multiformats": "^13.0.0",
|
||||
"p-defer": "^4.0.0",
|
||||
"p-queue": "^8.0.1",
|
||||
"uint8arrays": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@helia/delegated-routing-v1-http-api-client/node_modules/multiformats": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz",
|
||||
"integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ=="
|
||||
},
|
||||
"node_modules/@heroicons/react": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.3.tgz",
|
||||
@@ -5257,6 +5285,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-readablestream-to-it": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/browser-readablestream-to-it/-/browser-readablestream-to-it-2.0.6.tgz",
|
||||
"integrity": "sha512-csJm66U/gTC6VHjeaOaziK6Y6ENdrzlNLdXnsdnvGX+3hGvedkxTyiMk2WbgKR8F15ACxDLJhDuE/cmovLPBQQ=="
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
||||
@@ -5451,6 +5484,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/cborg": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.0.tgz",
|
||||
"integrity": "sha512-q6cFW5m3KxfP/9xGI3yGLaC1l5DP6DWM9IvjiJojnIwohL5CQDl02EXViPV852mOfQo+7PJGPN01MI87vFGzyA==",
|
||||
"bin": {
|
||||
"cborg": "lib/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"license": "MIT",
|
||||
@@ -5860,6 +5901,18 @@
|
||||
"it-take": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/datastore-idb": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/datastore-idb/-/datastore-idb-2.1.9.tgz",
|
||||
"integrity": "sha512-o1LAE2VgVMeEWOP/zHYHV6MetGzbN+C72nRRUFwPXk0xLopsPN9wlH73D3De3pyDZKoKp2875XDFpRkDJ8mGWA==",
|
||||
"dependencies": {
|
||||
"datastore-core": "^9.0.0",
|
||||
"idb": "^8.0.0",
|
||||
"interface-datastore": "^8.0.0",
|
||||
"it-filter": "^3.0.4",
|
||||
"it-sort": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||
@@ -7607,6 +7660,11 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz",
|
||||
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -7725,6 +7783,30 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ipns": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ipns/-/ipns-9.1.0.tgz",
|
||||
"integrity": "sha512-up2o1Qx9tSSfh73k69j3/Acacua6JbffTe5xA8+/fv6ibkQyhriMPHlgae1896DwmQkJrusKgBs7EAOi3yrO2w==",
|
||||
"dependencies": {
|
||||
"@libp2p/crypto": "^4.0.0",
|
||||
"@libp2p/interface": "^1.1.0",
|
||||
"@libp2p/logger": "^4.0.3",
|
||||
"@libp2p/peer-id": "^4.0.3",
|
||||
"cborg": "^4.0.1",
|
||||
"err-code": "^3.0.1",
|
||||
"interface-datastore": "^8.1.0",
|
||||
"multiformats": "^13.0.0",
|
||||
"protons-runtime": "^5.2.1",
|
||||
"timestamp-nano": "^1.0.0",
|
||||
"uint8arraylist": "^2.4.8",
|
||||
"uint8arrays": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ipns/node_modules/multiformats": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz",
|
||||
"integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ=="
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.1.1",
|
||||
"dev": true,
|
||||
@@ -8230,6 +8312,11 @@
|
||||
"it-peekable": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/it-first": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/it-first/-/it-first-3.0.5.tgz",
|
||||
"integrity": "sha512-LAZ4/pOU1GCDR0VjsAhfVixedlXbyXYKvgmUWl9AG9Ran67OySb1PB41EkILYLeiD9UNRxT05JxEjU4e6rlKHg=="
|
||||
},
|
||||
"node_modules/it-foreach": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/it-foreach/-/it-foreach-2.0.6.tgz",
|
||||
@@ -8287,6 +8374,11 @@
|
||||
"it-pushable": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/it-ndjson": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/it-ndjson/-/it-ndjson-1.0.6.tgz",
|
||||
"integrity": "sha512-44QB+rmfB2C8L2WDv03+J3kcOQYuIPBP3KBN+szZTEsvvNyoQEQSnOXoGouNpRfgMA1rEqqoK6roenCJaBeYSQ=="
|
||||
},
|
||||
"node_modules/it-pair": {
|
||||
"version": "2.0.6",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
@@ -12372,6 +12464,14 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/timestamp-nano": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/timestamp-nano/-/timestamp-nano-1.0.1.tgz",
|
||||
"integrity": "sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA==",
|
||||
"engines": {
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/titleize": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"@chainsafe/libp2p-yamux": "^6.0.2",
|
||||
"@download/blockies": "^1.0.3",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@helia/delegated-routing-v1-http-api-client": "^3.0.1",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@libp2p/bootstrap": "^10.0.20",
|
||||
"@libp2p/circuit-relay-v2": "^1.0.20",
|
||||
@@ -22,6 +23,7 @@
|
||||
"@libp2p/websockets": "^8.0.20",
|
||||
"@libp2p/webtransport": "^4.0.27",
|
||||
"@multiformats/multiaddr": "^12.2.1",
|
||||
"datastore-idb": "^2.1.9",
|
||||
"debug": "^4.3.4",
|
||||
"it-length-prefixed": "^9.0.4",
|
||||
"it-map": "^3.0.5",
|
||||
|
||||
@@ -3,159 +3,25 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { Message } from '@libp2p/interface'
|
||||
import { CHAT_FILE_TOPIC, CHAT_TOPIC, FILE_EXCHANGE_PROTOCOL } from '@/lib/constants'
|
||||
import { createIcon } from '@download/blockies'
|
||||
import { ChatMessage, useChatContext } from '../context/chat-ctx'
|
||||
import { ChatFile, ChatMessage, useChatContext } from '../context/chat-ctx'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ChatFile, useFileChatContext } from '@/context/file-ctx'
|
||||
import { pipe } from 'it-pipe'
|
||||
import map from 'it-map'
|
||||
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
||||
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
||||
import * as lp from 'it-length-prefixed'
|
||||
import { MessageComponent } from './message'
|
||||
|
||||
interface MessageProps extends ChatMessage { }
|
||||
|
||||
function Message({ msg, fileObjectUrl, from, peerId }: MessageProps) {
|
||||
const msgref = React.useRef<HTMLLIElement>(null)
|
||||
const { libp2p } = useLibp2pContext()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const icon = createIcon({
|
||||
seed: peerId,
|
||||
size: 15,
|
||||
scale: 3,
|
||||
})
|
||||
icon.className = 'rounded mr-2 max-h-10 max-w-10'
|
||||
const childrenCount = msgref.current?.childElementCount
|
||||
// Prevent inserting an icon more than once.
|
||||
if (childrenCount && childrenCount < 2) {
|
||||
msgref.current?.insertBefore(icon, msgref.current?.firstChild)
|
||||
}
|
||||
}, [peerId])
|
||||
|
||||
return (
|
||||
<li ref={msgref} className={`flex ${from === 'me' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div
|
||||
|
||||
className="flex relative max-w-xl px-4 py-2 text-gray-700 rounded shadow bg-white"
|
||||
>
|
||||
<div className="block">
|
||||
{msg}
|
||||
<p>{fileObjectUrl ? <a href={fileObjectUrl} target="_blank"><b>Download</b></a> : ""}</p>
|
||||
<p className="italic text-gray-400">{peerId !== libp2p.peerId.toString() ? `from: ${peerId.slice(-4)}` : null} </p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ChatContainer() {
|
||||
const { libp2p } = useLibp2pContext()
|
||||
const { messageHistory, setMessageHistory } = useChatContext();
|
||||
const { files, setFiles } = useFileChatContext();
|
||||
const { messageHistory, setMessageHistory, files, setFiles } = useChatContext();
|
||||
const [input, setInput] = useState<string>('')
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Effect hook to subscribe to pubsub events and update the message state hook
|
||||
useEffect(() => {
|
||||
const messageCB = async (evt: CustomEvent<Message>) => {
|
||||
console.log('gossipsub console log', evt.detail)
|
||||
// FIXME: Why does 'from' not exist on type 'Message'?
|
||||
const { topic, data } = evt.detail
|
||||
|
||||
switch (topic) {
|
||||
case CHAT_TOPIC: {
|
||||
chatMessageCB(evt, topic, data)
|
||||
break
|
||||
}
|
||||
case CHAT_FILE_TOPIC: {
|
||||
chatFileMessageCB(evt, topic, data)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unexpected gossipsub topic: ${topic}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageCBWrapper = (evt: Event) => {
|
||||
const customEvent = evt as CustomEvent<Message>;
|
||||
(async () => messageCB(customEvent))();
|
||||
};
|
||||
|
||||
const chatMessageCB = (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
|
||||
const msg = new TextDecoder().decode(data)
|
||||
console.log(`${topic}: ${msg}`)
|
||||
|
||||
// Append signed messages, otherwise discard
|
||||
if (evt.detail.type === 'signed') {
|
||||
setMessageHistory([...messageHistory, { msg, fileObjectUrl: undefined, from: 'other', peerId: evt.detail.from.toString() }])
|
||||
}
|
||||
}
|
||||
|
||||
const chatFileMessageCB = async (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
|
||||
const fileId = new TextDecoder().decode(data)
|
||||
|
||||
// if the message isn't signed, discard it.
|
||||
if (evt.detail.type !== 'signed') {
|
||||
return
|
||||
}
|
||||
const senderPeerId = evt.detail.from;
|
||||
|
||||
try {
|
||||
const stream = await libp2p.dialProtocol(senderPeerId, FILE_EXCHANGE_PROTOCOL)
|
||||
await pipe(
|
||||
[uint8ArrayFromString(fileId)],
|
||||
(source) => lp.encode(source),
|
||||
stream,
|
||||
(source) => lp.decode(source),
|
||||
async function(source) {
|
||||
for await (const data of source) {
|
||||
const body: Uint8Array = data.subarray()
|
||||
console.log(`request_response: response received: size:${body.length}`)
|
||||
|
||||
const msg: ChatMessage = {
|
||||
msg: newChatFileMessage(fileId, body),
|
||||
fileObjectUrl: window.URL.createObjectURL(new Blob([body])),
|
||||
from: 'other',
|
||||
peerId: senderPeerId.toString(),
|
||||
}
|
||||
setMessageHistory([...messageHistory, msg])
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
libp2p.services.pubsub.addEventListener('message', messageCBWrapper)
|
||||
|
||||
libp2p.handle(FILE_EXCHANGE_PROTOCOL, ({ stream }) => {
|
||||
pipe(
|
||||
stream.source,
|
||||
(source) => lp.decode(source),
|
||||
(source) => map(source, async (msg) => {
|
||||
const fileId = uint8ArrayToString(msg.subarray())
|
||||
const file = files.get(fileId)!
|
||||
return file.body
|
||||
}),
|
||||
(source) => lp.encode(source),
|
||||
stream.sink,
|
||||
)
|
||||
})
|
||||
|
||||
return () => {
|
||||
(async () => {
|
||||
// Cleanup handlers 👇
|
||||
// libp2p.services.pubsub.unsubscribe(CHAT_TOPIC)
|
||||
// libp2p.services.pubsub.unsubscribe(CHAT_FILE_TOPIC)
|
||||
libp2p.services.pubsub.removeEventListener('message', messageCBWrapper)
|
||||
await libp2p.unhandle(FILE_EXCHANGE_PROTOCOL)
|
||||
})();
|
||||
}
|
||||
}, [libp2p, messageHistory, setMessageHistory, files])
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
if (input === '') return
|
||||
|
||||
@@ -284,7 +150,7 @@ export default function ChatContainer() {
|
||||
<ul className="space-y-2">
|
||||
{/* messages start */}
|
||||
{messageHistory.map(({ msg, fileObjectUrl, from, peerId }, idx) => (
|
||||
<Message key={idx} msg={msg} fileObjectUrl={fileObjectUrl} from={from} peerId={peerId} />
|
||||
<MessageComponent key={idx} msg={msg} fileObjectUrl={fileObjectUrl} from={from} peerId={peerId} />
|
||||
))}
|
||||
{/* messages end */}
|
||||
</ul>
|
||||
@@ -370,95 +236,3 @@ export default function ChatContainer() {
|
||||
)
|
||||
}
|
||||
|
||||
export function RoomList() {
|
||||
return (
|
||||
<div className="border-r border-gray-300 lg:col-span-1">
|
||||
<div className="mx-3 my-3">
|
||||
<div className="relative text-gray-600">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-6 h-6 text-gray-300"
|
||||
>
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
className="block w-full py-2 pl-10 bg-gray-100 rounded outline-none"
|
||||
name="search"
|
||||
placeholder="Search"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="overflow-auto h-[32rem]">
|
||||
<h2 className="my-2 mb-2 ml-2 text-lg text-gray-600">Chats</h2>
|
||||
<li>
|
||||
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
|
||||
<img
|
||||
className="object-cover w-10 h-10 rounded-full"
|
||||
src="https://github.com/2color.png"
|
||||
alt="username"
|
||||
/>
|
||||
<div className="w-full pb-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="block ml-2 font-semibold text-gray-600">
|
||||
Daniel
|
||||
</span>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
25 minutes
|
||||
</span>
|
||||
</div>
|
||||
<span className="block ml-2 text-sm text-gray-600">bye</span>
|
||||
</div>
|
||||
</a>
|
||||
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out bg-gray-100 border-b border-gray-300 cursor-pointer focus:outline-none">
|
||||
<img
|
||||
className="object-cover w-10 h-10 rounded-full"
|
||||
src="https://github.com/achingbrain.png"
|
||||
alt="username"
|
||||
/>
|
||||
<div className="w-full pb-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="block ml-2 font-semibold text-gray-600">
|
||||
Alex
|
||||
</span>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
50 minutes
|
||||
</span>
|
||||
</div>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
Good night
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
|
||||
<img
|
||||
className="object-cover w-10 h-10 rounded-full"
|
||||
src="https://github.com/hannahhoward.png"
|
||||
alt="username"
|
||||
/>
|
||||
<div className="w-full pb-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="block ml-2 font-semibold text-gray-600">
|
||||
Hannah
|
||||
</span>
|
||||
<span className="block ml-2 text-sm text-gray-600">6 hour</span>
|
||||
</div>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
Good Morning
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
43
js-peer/src/components/message.tsx
Normal file
43
js-peer/src/components/message.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useLibp2pContext } from '@/context/ctx'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createIcon } from '@download/blockies'
|
||||
import { ChatMessage } from '@/context/chat-ctx'
|
||||
|
||||
|
||||
interface MessageProps extends ChatMessage { }
|
||||
|
||||
|
||||
export function MessageComponent({ msg, fileObjectUrl, from, peerId }: MessageProps) {
|
||||
const msgref = React.useRef<HTMLLIElement>(null)
|
||||
const { libp2p } = useLibp2pContext()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const icon = createIcon({
|
||||
seed: peerId,
|
||||
size: 15,
|
||||
scale: 3,
|
||||
})
|
||||
icon.className = 'rounded mr-2 max-h-10 max-w-10'
|
||||
const childrenCount = msgref.current?.childElementCount
|
||||
// Prevent inserting an icon more than once.
|
||||
if (childrenCount && childrenCount < 2) {
|
||||
msgref.current?.insertBefore(icon, msgref.current?.firstChild)
|
||||
}
|
||||
}, [peerId])
|
||||
|
||||
return (
|
||||
<li ref={msgref} className={`flex ${from === 'me' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div
|
||||
|
||||
className="flex relative max-w-xl px-4 py-2 text-gray-700 rounded shadow bg-white"
|
||||
>
|
||||
<div className="block">
|
||||
{msg}
|
||||
<p>{fileObjectUrl ? <a href={fileObjectUrl} target="_blank"><b>Download</b></a> : ""}</p>
|
||||
<p className="italic text-gray-400">{peerId !== libp2p.peerId.toString() ? `from: ${peerId.slice(-4)}` : null} </p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
92
js-peer/src/components/room-list.tsx
Normal file
92
js-peer/src/components/room-list.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export function RoomList() {
|
||||
return (
|
||||
<div className="border-r border-gray-300 lg:col-span-1">
|
||||
<div className="mx-3 my-3">
|
||||
<div className="relative text-gray-600">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-6 h-6 text-gray-300"
|
||||
>
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
className="block w-full py-2 pl-10 bg-gray-100 rounded outline-none"
|
||||
name="search"
|
||||
placeholder="Search"
|
||||
required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="overflow-auto h-[32rem]">
|
||||
<h2 className="my-2 mb-2 ml-2 text-lg text-gray-600">Chats</h2>
|
||||
<li>
|
||||
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
|
||||
<img
|
||||
className="object-cover w-10 h-10 rounded-full"
|
||||
src="https://github.com/2color.png"
|
||||
alt="username" />
|
||||
<div className="w-full pb-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="block ml-2 font-semibold text-gray-600">
|
||||
Daniel
|
||||
</span>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
25 minutes
|
||||
</span>
|
||||
</div>
|
||||
<span className="block ml-2 text-sm text-gray-600">bye</span>
|
||||
</div>
|
||||
</a>
|
||||
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out bg-gray-100 border-b border-gray-300 cursor-pointer focus:outline-none">
|
||||
<img
|
||||
className="object-cover w-10 h-10 rounded-full"
|
||||
src="https://github.com/achingbrain.png"
|
||||
alt="username" />
|
||||
<div className="w-full pb-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="block ml-2 font-semibold text-gray-600">
|
||||
Alex
|
||||
</span>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
50 minutes
|
||||
</span>
|
||||
</div>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
Good night
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
|
||||
<img
|
||||
className="object-cover w-10 h-10 rounded-full"
|
||||
src="https://github.com/hannahhoward.png"
|
||||
alt="username" />
|
||||
<div className="w-full pb-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="block ml-2 font-semibold text-gray-600">
|
||||
Hannah
|
||||
</span>
|
||||
<span className="block ml-2 text-sm text-gray-600">6 hour</span>
|
||||
</div>
|
||||
<span className="block ml-2 text-sm text-gray-600">
|
||||
Good Morning
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useLibp2pContext } from './ctx';
|
||||
import type { Message } from '@libp2p/interface'
|
||||
import { CHAT_FILE_TOPIC, CHAT_TOPIC, FILE_EXCHANGE_PROTOCOL } from '@/lib/constants'
|
||||
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
||||
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
||||
import { pipe } from 'it-pipe'
|
||||
import map from 'it-map'
|
||||
import * as lp from 'it-length-prefixed'
|
||||
|
||||
|
||||
export interface ChatMessage {
|
||||
msg: string
|
||||
@@ -7,13 +16,23 @@ export interface ChatMessage {
|
||||
peerId: string
|
||||
}
|
||||
|
||||
export interface ChatFile {
|
||||
id: string
|
||||
body: Uint8Array
|
||||
sender: string
|
||||
}
|
||||
|
||||
export interface ChatContextInterface {
|
||||
messageHistory: ChatMessage[];
|
||||
setMessageHistory: (messageHistory: ChatMessage[]) => void;
|
||||
files: Map<string, ChatFile>
|
||||
setFiles: (files: Map<string, ChatFile>) => void;
|
||||
}
|
||||
export const chatContext = createContext<ChatContextInterface>({
|
||||
messageHistory: [],
|
||||
setMessageHistory: () => { }
|
||||
files: new Map<string, ChatFile>(),
|
||||
setMessageHistory: () => { },
|
||||
setFiles: () => { }
|
||||
})
|
||||
|
||||
export const useChatContext = () => {
|
||||
@@ -22,9 +41,107 @@ export const useChatContext = () => {
|
||||
|
||||
export const ChatProvider = ({ children }: any) => {
|
||||
const [messageHistory, setMessageHistory] = useState<ChatMessage[]>([]);
|
||||
const [files, setFiles] = useState<Map<string, ChatFile>>(new Map<string, ChatFile>());
|
||||
const { libp2p } = useLibp2pContext()
|
||||
|
||||
const messageCB = (evt: CustomEvent<Message>) => {
|
||||
console.log('gossipsub console log', evt.detail)
|
||||
// FIXME: Why does 'from' not exist on type 'Message'?
|
||||
const { topic, data } = evt.detail
|
||||
|
||||
switch (topic) {
|
||||
case CHAT_TOPIC: {
|
||||
chatMessageCB(evt, topic, data)
|
||||
break
|
||||
}
|
||||
case CHAT_FILE_TOPIC: {
|
||||
chatFileMessageCB(evt, topic, data)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unexpected gossipsub topic: ${topic}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chatMessageCB = (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
|
||||
const msg = new TextDecoder().decode(data)
|
||||
console.log(`${topic}: ${msg}`)
|
||||
|
||||
// Append signed messages, otherwise discard
|
||||
if (evt.detail.type === 'signed') {
|
||||
setMessageHistory([...messageHistory, { msg, fileObjectUrl: undefined, from: 'other', peerId: evt.detail.from.toString() }])
|
||||
}
|
||||
}
|
||||
|
||||
const chatFileMessageCB = async (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
|
||||
const newChatFileMessage = (id: string, body: Uint8Array) => {
|
||||
return `File: ${id} (${body.length} bytes)`
|
||||
}
|
||||
const fileId = new TextDecoder().decode(data)
|
||||
|
||||
// if the message isn't signed, discard it.
|
||||
if (evt.detail.type !== 'signed') {
|
||||
return
|
||||
}
|
||||
const senderPeerId = evt.detail.from;
|
||||
|
||||
try {
|
||||
const stream = await libp2p.dialProtocol(senderPeerId, FILE_EXCHANGE_PROTOCOL)
|
||||
await pipe(
|
||||
[uint8ArrayFromString(fileId)],
|
||||
(source) => lp.encode(source),
|
||||
stream,
|
||||
(source) => lp.decode(source),
|
||||
async function(source) {
|
||||
for await (const data of source) {
|
||||
const body: Uint8Array = data.subarray()
|
||||
console.log(`request_response: response received: size:${body.length}`)
|
||||
|
||||
const msg: ChatMessage = {
|
||||
msg: newChatFileMessage(fileId, body),
|
||||
fileObjectUrl: window.URL.createObjectURL(new Blob([body])),
|
||||
from: 'other',
|
||||
peerId: senderPeerId.toString(),
|
||||
}
|
||||
setMessageHistory([...messageHistory, msg])
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
libp2p.services.pubsub.addEventListener('message', messageCB)
|
||||
|
||||
libp2p.handle(FILE_EXCHANGE_PROTOCOL, ({ stream }) => {
|
||||
pipe(
|
||||
stream.source,
|
||||
(source) => lp.decode(source),
|
||||
(source) => map(source, async (msg) => {
|
||||
const fileId = uint8ArrayToString(msg.subarray())
|
||||
const file = files.get(fileId)!
|
||||
return file.body
|
||||
}),
|
||||
(source) => lp.encode(source),
|
||||
stream.sink,
|
||||
)
|
||||
})
|
||||
|
||||
return () => {
|
||||
(async () => {
|
||||
// Cleanup handlers 👇
|
||||
libp2p.services.pubsub.removeEventListener('message', messageCB)
|
||||
await libp2p.unhandle(FILE_EXCHANGE_PROTOCOL)
|
||||
})();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<chatContext.Provider value={{ messageHistory, setMessageHistory }}>
|
||||
<chatContext.Provider value={{ messageHistory, setMessageHistory, files, setFiles }}>
|
||||
{children}
|
||||
</chatContext.Provider>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,8 @@ import { startLibp2p } from '../lib/libp2p'
|
||||
import { ChatProvider } from './chat-ctx'
|
||||
import { PeerProvider } from './peer-ctx'
|
||||
import { ListenAddressesProvider } from './listen-addresses-ctx'
|
||||
import { PubSub } from '@libp2p/interface'
|
||||
import { PubSub, } from '@libp2p/interface'
|
||||
import { Identify } from '@libp2p/identify'
|
||||
|
||||
// 👇 The context type will be avilable "anywhere" in the app
|
||||
interface Libp2pContextInterface {
|
||||
@@ -39,7 +40,7 @@ export function AppWrapper({ children }: WrapperProps) {
|
||||
// @ts-ignore
|
||||
window.libp2p = libp2p
|
||||
|
||||
setLibp2p(libp2p as Libp2p<{ pubsub: any; dht: any; identify: any }>)
|
||||
setLibp2p(libp2p as Libp2p<{ pubsub: PubSub; identify: Identify }>)
|
||||
} catch (e) {
|
||||
console.error('failed to start libp2p', e)
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
export interface ChatFile {
|
||||
id: string
|
||||
body: Uint8Array
|
||||
sender: string
|
||||
}
|
||||
|
||||
export interface FileChatContextInterface {
|
||||
files: Map<string, ChatFile>
|
||||
setFiles: (files: Map<string, ChatFile>) => void;
|
||||
}
|
||||
export const fileContext = createContext<FileChatContextInterface>({
|
||||
files: new Map<string, ChatFile>(),
|
||||
setFiles: () => { }
|
||||
})
|
||||
|
||||
export const useFileChatContext = () => {
|
||||
return useContext(fileContext);
|
||||
};
|
||||
|
||||
export const FileProvider = ({ children }: any) => {
|
||||
const [files, setFiles] = useState<Map<string, ChatFile>>(new Map<string, ChatFile>());
|
||||
|
||||
return (
|
||||
<fileContext.Provider value={{ files, setFiles }}>
|
||||
{children}
|
||||
</fileContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -4,5 +4,9 @@ export const FILE_EXCHANGE_PROTOCOL = "/universal-connectivity-file/1"
|
||||
|
||||
export const CIRCUIT_RELAY_CODE = 290
|
||||
|
||||
export const WEBRTC_BOOTSTRAP_NODE = "/ip4/147.28.186.157/udp/9090/webrtc-direct/certhash/uEiBbC9bbdvraVWDvcvCEdJAWDymmUqiJQ964FuyEq0hELw/p2p/12D3KooWGahRw3ZnM4gAyd9FK75v4Bp5keFYTvkcAwhpEm28wbV3"
|
||||
export const WEBTRANSPORT_BOOTSTRAP_NODE = "/ip4/147.28.186.157/udp/9095/quic-v1/webtransport/certhash/uEiCmLPMgXJ1F1wQ-OgOWJEVa_SYB_jLSf5IkQ_d3V98GBQ/certhash/uEiB-ti6URtr64LV8HYDMvZzzzrb1iNEIT-vGY0yd6UYk2g/p2p/12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr"
|
||||
// 👇 The multiaddrs below are ephemeral and not recommended for use as they expire after a couple of weeks. Instead PeerIDs with peer routing is used
|
||||
// export const WEBRTC_BOOTSTRAP_MULTIADDR = "/ip4/147.28.186.157/udp/9090/webrtc-direct/certhash/uEiBbC9bbdvraVWDvcvCEdJAWDymmUqiJQ964FuyEq0hELw/p2p/12D3KooWGahRw3ZnM4gAyd9FK75v4Bp5keFYTvkcAwhpEm28wbV3"
|
||||
// export const WEBTRANSPORT_BOOTSTRAP_MULTIADDR = "/ip4/147.28.186.157/udp/9095/quic-v1/webtransport/certhash/uEiCmLPMgXJ1F1wQ-OgOWJEVa_SYB_jLSf5IkQ_d3V98GBQ/certhash/uEiB-ti6URtr64LV8HYDMvZzzzrb1iNEIT-vGY0yd6UYk2g/p2p/12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr"
|
||||
|
||||
export const WEBRTC_BOOTSTRAP_PEER_ID = "12D3KooWGahRw3ZnM4gAyd9FK75v4Bp5keFYTvkcAwhpEm28wbV3"
|
||||
export const WEBTRANSPORT_BOOTSTRAP_PEER_ID = "12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr"
|
||||
@@ -1,42 +1,46 @@
|
||||
import { IDBDatastore } from 'datastore-idb'
|
||||
import { createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client'
|
||||
import { createLibp2p, Libp2p } from 'libp2p'
|
||||
import { identify } from '@libp2p/identify'
|
||||
import {peerIdFromString} from '@libp2p/peer-id'
|
||||
import { noise } from '@chainsafe/libp2p-noise'
|
||||
import { yamux } from '@chainsafe/libp2p-yamux'
|
||||
import { bootstrap } from '@libp2p/bootstrap'
|
||||
import { kadDHT } from '@libp2p/kad-dht'
|
||||
import {
|
||||
Multiaddr,
|
||||
} from '@multiformats/multiaddr'
|
||||
import { Multiaddr } from '@multiformats/multiaddr'
|
||||
import { sha256 } from 'multiformats/hashes/sha2'
|
||||
import type { Message, SignedMessage } from '@libp2p/interface'
|
||||
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
|
||||
import { webSockets } from '@libp2p/websockets'
|
||||
import { webTransport } from '@libp2p/webtransport'
|
||||
import { webRTC, webRTCDirect } from '@libp2p/webrtc'
|
||||
import { CHAT_FILE_TOPIC, CHAT_TOPIC, WEBRTC_BOOTSTRAP_NODE, WEBTRANSPORT_BOOTSTRAP_NODE } from './constants'
|
||||
import { CHAT_FILE_TOPIC, CHAT_TOPIC, WEBRTC_BOOTSTRAP_PEER_ID, WEBTRANSPORT_BOOTSTRAP_PEER_ID } from './constants'
|
||||
import * as filters from "@libp2p/websockets/filters"
|
||||
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||
|
||||
export async function startLibp2p() {
|
||||
// enable verbose logging in browser console to view debug logs
|
||||
// localStorage.debug = 'libp2p*,-*:trace'
|
||||
localStorage.debug = 'libp2p*,-*:trace'
|
||||
|
||||
// application-specific data lives in the datastore
|
||||
const datastore = new IDBDatastore('universal-connectivity')
|
||||
|
||||
await datastore.open()
|
||||
|
||||
const libp2p = await createLibp2p({
|
||||
datastore,
|
||||
addresses: {
|
||||
listen: [
|
||||
// 👇 Listen for webRTC connection
|
||||
'/webrtc'
|
||||
]
|
||||
},
|
||||
transports: [
|
||||
webTransport(),
|
||||
webSockets({
|
||||
filter: filters.all,
|
||||
}),
|
||||
webSockets(),
|
||||
webRTC({
|
||||
rtcConfiguration: {
|
||||
iceServers: [{
|
||||
// STUN servers help the browser discover its own public IPs
|
||||
urls: [
|
||||
'stun:stun.l.google.com:19302',
|
||||
'stun:global.stun.twilio.com:3478'
|
||||
@@ -45,36 +49,45 @@ export async function startLibp2p() {
|
||||
}
|
||||
}),
|
||||
webRTCDirect(),
|
||||
// 👇 Required to create circuit relay reservations in order to hole punch browser-to-browser WebRTC connections
|
||||
circuitRelayTransport({
|
||||
// When set to >0, this will look up the magic CID in order to discover circuit relay peers it can create a reservation with
|
||||
discoverRelays: 1,
|
||||
})
|
||||
],
|
||||
connectionManager: {
|
||||
maxConnections: 10,
|
||||
minConnections: 5
|
||||
minConnections: 3
|
||||
},
|
||||
connectionEncryption: [noise()],
|
||||
streamMuxers: [yamux()],
|
||||
connectionGater: {
|
||||
denyDialMultiaddr: async () => false,
|
||||
},
|
||||
peerDiscovery: [
|
||||
bootstrap({
|
||||
list: [
|
||||
WEBRTC_BOOTSTRAP_NODE,
|
||||
WEBTRANSPORT_BOOTSTRAP_NODE,
|
||||
],
|
||||
}),
|
||||
],
|
||||
// The app-specific go and rust peers use WebTransport and WebRTC-direct which have ephemeral multiadrrs that change.
|
||||
// Thus, we dial them using only their peer id below, with delegated routing to discovery their multiaddrs
|
||||
// peerDiscovery: [
|
||||
// bootstrap({
|
||||
// list: [
|
||||
// '12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr'
|
||||
// WEBRTC_BOOTSTRAP_NODE,
|
||||
// WEBTRANSPORT_BOOTSTRAP_NODE,
|
||||
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
|
||||
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa',
|
||||
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
|
||||
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt',
|
||||
// ],
|
||||
// }),
|
||||
// ],
|
||||
services: {
|
||||
pubsub: gossipsub({
|
||||
allowPublishToZeroTopicPeers: true,
|
||||
msgIdFn: msgIdFnStrictNoSign,
|
||||
ignoreDuplicatePublishError: true,
|
||||
}),
|
||||
dht: kadDHT({
|
||||
clientMode: true,
|
||||
}),
|
||||
// Delegated routing helps us discover the ephemeral multiaddrs of the dedicated go and rust bootstrap peers
|
||||
// This relies on the public delegated routing endpoint https://docs.ipfs.tech/concepts/public-utilities/#delegated-routing
|
||||
delegatedRouting: () => createDelegatedRoutingV1HttpApiClient('https://delegated-ipfs.dev'),
|
||||
identify: identify()
|
||||
},
|
||||
})
|
||||
@@ -82,6 +95,13 @@ export async function startLibp2p() {
|
||||
libp2p.services.pubsub.subscribe(CHAT_TOPIC)
|
||||
libp2p.services.pubsub.subscribe(CHAT_FILE_TOPIC)
|
||||
|
||||
// Try connecting to bootstrap ppers
|
||||
Promise.all([
|
||||
libp2p.dial(peerIdFromString(WEBRTC_BOOTSTRAP_PEER_ID)),
|
||||
libp2p.dial(peerIdFromString(WEBTRANSPORT_BOOTSTRAP_PEER_ID))
|
||||
])
|
||||
.catch(e => {console.log('woot', e)})
|
||||
|
||||
libp2p.addEventListener('self:peer:update', ({ detail: { peer } }) => {
|
||||
const multiaddrs = peer.addresses.map(({ multiaddr }) => multiaddr)
|
||||
|
||||
|
||||
@@ -106,8 +106,6 @@ export default function Home() {
|
||||
[libp2p, maddr],
|
||||
)
|
||||
|
||||
// handleConnectToMultiaddr
|
||||
|
||||
const handleMultiaddrChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setMultiaddr(e.target.value)
|
||||
|
||||
Reference in New Issue
Block a user