js-peer: overhaul ui and load that chat by default (#233)

* feat: overhaul ui and load chat by default

* fix: simplify nav bar and load chat on index route

* fix: linting

---------

Co-authored-by: Daniel N <2color@users.noreply.github.com>
This commit is contained in:
Daniel Norman
2025-02-27 09:22:09 +01:00
committed by GitHub
parent 71586b267d
commit ec11a59480
8 changed files with 388 additions and 273 deletions

View File

@@ -4,7 +4,11 @@ import React, { useEffect, useState } from 'react'
import type { PeerId } from '@libp2p/interface'
import { PeerWrapper } from './peer'
export function ChatPeerList() {
interface ChatPeerListProps {
hideHeader?: boolean
}
export function ChatPeerList({ hideHeader = false }: ChatPeerListProps) {
const { libp2p } = useLibp2pContext()
const [subscribers, setSubscribers] = useState<PeerId[]>([])
@@ -22,8 +26,8 @@ export function ChatPeerList() {
return (
<div className="border-l border-gray-300 lg:col-span-1">
<h2 className="my-2 mb-2 ml-2 text-lg text-gray-600">Peers</h2>
<div className="overflow-auto h-[32rem]">
{!hideHeader && <h2 className="my-2 mb-2 ml-2 text-lg text-gray-600">Peers</h2>}
<div className="overflow-auto h-[20rem] lg:h-[32rem]">
<div className="px-3 py-2 border-b border-gray-300 focus:outline-none">
{<PeerWrapper peer={libp2p.peerId} self withName={true} withUnread={false} />}
</div>

View File

@@ -7,6 +7,7 @@ import { Message } from './message'
import { forComponent } from '@/lib/logger'
import { ChatPeerList } from './chat-peer-list'
import { ChevronLeftIcon } from '@heroicons/react/20/solid'
import { UsersIcon } from '@heroicons/react/24/outline'
import Blockies from 'react-18-blockies'
import { peerIdFromString } from '@libp2p/peer-id'
@@ -22,6 +23,7 @@ export default function ChatContainer() {
const [input, setInput] = useState<string>('')
const fileRef = useRef<HTMLInputElement>(null)
const [messages, setMessages] = useState<ChatMessage[]>([])
const [showMobilePeerList, setShowMobilePeerList] = useState(false)
// Send message to public chat over gossipsub
const sendPublicMessage = useCallback(async () => {
@@ -183,6 +185,10 @@ export default function ChatContainer() {
setMessages(messageHistory)
}
const toggleMobilePeerList = () => {
setShowMobilePeerList(!showMobilePeerList)
}
useEffect(() => {
// assumes a chat room is a peerId thus a direct message
if (roomId === PUBLIC_CHAT_ROOM_ID) {
@@ -193,26 +199,71 @@ export default function ChatContainer() {
}, [roomId, directMessages, messageHistory])
return (
<div className="container mx-auto">
<div className="min-w-full border rounded lg:grid lg:grid-cols-6">
<div className="lg:col-span-5 lg:block">
<div className="container mx-auto w-full px-0">
<div className="min-w-full border-0 rounded-none lg:rounded grid grid-cols-1 lg:grid-cols-6">
<div className="col-span-1 lg:col-span-5">
<div className="w-full">
<div className="relative flex items-center p-3 border-b border-gray-300">
{roomId === PUBLIC_CHAT_ROOM_ID && (
<span className="block ml-2 font-bold text-gray-600">{PUBLIC_CHAT_ROOM_NAME}</span>
<>
<span className="block ml-2 font-bold text-gray-600">{PUBLIC_CHAT_ROOM_NAME}</span>
<button
onClick={toggleMobilePeerList}
className="ml-auto lg:hidden flex items-center text-gray-500 hover:text-gray-700"
aria-label="Toggle peer list"
>
<UsersIcon className="h-5 w-5" />
<span className="ml-1 text-sm">Peers</span>
</button>
</>
)}
{roomId !== PUBLIC_CHAT_ROOM_ID && (
<>
<Blockies seed={roomId} size={8} scale={3} className="rounded mr-2 max-h-10 max-w-10" />
<span className={`text-gray-500 flex`}>{roomId.toString().slice(-7)}</span>
<button onClick={handleBackToPublic} className="text-gray-500 flex ml-auto">
<ChevronLeftIcon className="w-6 h-6 text-gray-500" />
<span>Back to Public Chat</span>
</button>
<div className="flex items-center ml-auto">
<button
onClick={toggleMobilePeerList}
className="lg:hidden flex items-center text-gray-500 hover:text-gray-700 mr-4"
aria-label="Toggle peer list"
>
<UsersIcon className="h-5 w-5" />
<span className="ml-1 text-sm">Peers</span>
</button>
<button onClick={handleBackToPublic} className="text-gray-500 flex">
<ChevronLeftIcon className="w-6 h-6 text-gray-500" />
<span className="hidden sm:inline">Back to Public Chat</span>
<span className="sm:hidden">Back</span>
</button>
</div>
</>
)}
</div>
<div className="relative w-full flex flex-col-reverse p-3 overflow-y-auto h-[40rem] bg-gray-100">
{/* Show mobile peer list when toggled */}
{showMobilePeerList && (
<div className="lg:hidden border-b border-gray-300">
<div className="flex items-center justify-between p-2 bg-gray-50">
<h2 className="text-lg text-gray-600">Peers</h2>
<button
onClick={toggleMobilePeerList}
className="text-gray-500 hover:text-gray-700"
aria-label="Close peer list"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<ChatPeerList hideHeader={true} />
</div>
)}
<div className="relative w-full flex flex-col-reverse p-3 overflow-y-auto h-[calc(60vh-8rem)] sm:h-[40rem] bg-gray-100">
<ul className="space-y-2">
{messages.map(({ msgId, msg, fileObjectUrl, peerId, read, receivedAt }: ChatMessage) => (
<Message
@@ -229,7 +280,7 @@ export default function ChatContainer() {
</ul>
</div>
<div className="flex items-center justify-between w-full p-3 border-t border-gray-300">
<div className="flex items-center justify-between w-full p-2 sm:p-3 border-t border-gray-300">
<input
ref={fileRef}
className="hidden"
@@ -241,7 +292,7 @@ export default function ChatContainer() {
onClick={handleFileSend}
disabled={roomId !== PUBLIC_CHAT_ROOM_ID}
title={roomId === PUBLIC_CHAT_ROOM_ID ? 'Upload file' : "Unsupported in DM's"}
className={roomId === PUBLIC_CHAT_ROOM_ID ? '' : 'cursor-not-allowed'}
className={`${roomId === PUBLIC_CHAT_ROOM_ID ? '' : 'cursor-not-allowed'} p-1`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -265,7 +316,7 @@ export default function ChatContainer() {
onChange={handleInput}
type="text"
placeholder="Message"
className="block w-full py-2 pl-4 mx-3 bg-gray-100 rounded-full outline-none focus:text-gray-700"
className="block w-full py-2 pl-2 sm:pl-4 mx-2 sm:mx-3 bg-gray-100 rounded-full outline-none focus:text-gray-700 text-sm sm:text-base"
name="message"
required
/>
@@ -282,7 +333,9 @@ export default function ChatContainer() {
</div>
</div>
</div>
<ChatPeerList />
<div className="hidden lg:block">
<ChatPeerList />
</div>
</div>
</div>
)

View File

@@ -0,0 +1,20 @@
import { ServerIcon } from '@heroicons/react/24/outline'
import React from 'react'
interface ConnectionInfoButtonProps {
onClick: () => void
}
export default function ConnectionInfoButton({ onClick }: ConnectionInfoButtonProps) {
return (
<button
type="button"
onClick={onClick}
className="rounded-md bg-indigo-600 py-1.5 px-2 sm:px-3 text-xs sm:text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 flex items-center"
>
<ServerIcon className="h-4 w-4 sm:h-5 sm:w-5 mr-1 sm:mr-2" aria-hidden="true" />
<span className="hidden sm:inline">libp2p node info</span>
<span className="sm:hidden">Node</span>
</button>
)
}

View File

@@ -0,0 +1,252 @@
import { useLibp2pContext } from '@/context/ctx'
import type { PeerUpdate, Connection } from '@libp2p/interface'
import { useCallback, useEffect, useState } from 'react'
import { Multiaddr, multiaddr } from '@multiformats/multiaddr'
import { connectToMultiaddr } from '../lib/libp2p'
import Spinner from '@/components/spinner'
import PeerList from '@/components/peer-list'
import { Dialog, DialogTitle, DialogBody } from '@/components/dialog'
import {
XMarkIcon,
ChevronDownIcon,
ChevronUpIcon,
ClipboardIcon,
ClipboardDocumentCheckIcon,
} from '@heroicons/react/24/outline'
export default function ConnectionPanel({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const { libp2p } = useLibp2pContext()
const [connections, setConnections] = useState<Connection[]>([])
const [listenAddresses, setListenAddresses] = useState<Multiaddr[]>([])
const [maddr, setMultiaddr] = useState('')
const [dialling, setDialling] = useState(false)
const [err, setErr] = useState('')
const [addressesExpanded, setAddressesExpanded] = useState(true)
const [connectionsExpanded, setConnectionsExpanded] = useState(true)
const [copiedAddress, setCopiedAddress] = useState<number | null>(null)
const [copiedPeerId, setCopiedPeerId] = useState(false)
useEffect(() => {
const onConnection = () => {
const connections = libp2p.getConnections()
setConnections(connections)
}
onConnection()
libp2p.addEventListener('connection:open', onConnection)
libp2p.addEventListener('connection:close', onConnection)
return () => {
libp2p.removeEventListener('connection:open', onConnection)
libp2p.removeEventListener('connection:close', onConnection)
}
}, [libp2p, setConnections])
useEffect(() => {
const onPeerUpdate = (evt: CustomEvent<PeerUpdate>) => {
const maddrs = evt.detail.peer.addresses?.map((p) => p.multiaddr)
setListenAddresses(maddrs ?? [])
}
libp2p.addEventListener('self:peer:update', onPeerUpdate)
return () => {
libp2p.removeEventListener('self:peer:update', onPeerUpdate)
}
}, [libp2p, setListenAddresses])
const handleConnectToMultiaddr = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>) => {
setErr('')
if (!maddr) {
return
}
setDialling(true)
try {
await connectToMultiaddr(libp2p)(multiaddr(maddr))
} catch (e: any) {
setErr(e?.message ?? 'Error connecting')
} finally {
setDialling(false)
}
},
[libp2p, maddr],
)
const handleMultiaddrChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setMultiaddr(e.target.value)
},
[setMultiaddr],
)
const toggleAddresses = () => {
setAddressesExpanded(!addressesExpanded)
}
const toggleConnections = () => {
setConnectionsExpanded(!connectionsExpanded)
}
const copyAddress = (index: number, address: string) => {
navigator.clipboard.writeText(address).then(() => {
setCopiedAddress(index)
setTimeout(() => setCopiedAddress(null), 2000)
})
}
const copyPeerId = () => {
navigator.clipboard.writeText(libp2p.peerId.toString()).then(() => {
setCopiedPeerId(true)
setTimeout(() => setCopiedPeerId(false), 2000)
})
}
return (
<Dialog open={isOpen} onClose={onClose} size="2xl">
<div className="flex justify-between items-center">
<DialogTitle>Connection Information</DialogTitle>
<button type="button" className="rounded-md text-gray-400 hover:text-gray-500" onClick={onClose}>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<DialogBody>
<div className="space-y-6 px-2">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-sm font-medium text-gray-900">This PeerID:</h3>
<div className="mt-1 flex items-center bg-white p-2 rounded border border-gray-200">
<p className="text-sm text-gray-700 break-all font-mono flex-grow">{libp2p.peerId.toString()}</p>
<button
type="button"
onClick={copyPeerId}
className="ml-2 flex-shrink-0 text-gray-400 hover:text-gray-600"
title="Copy PeerID"
>
{copiedPeerId ? (
<ClipboardDocumentCheckIcon className="h-5 w-5 text-green-500" />
) : (
<ClipboardIcon className="h-5 w-5" />
)}
</button>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div
className="flex justify-between items-center cursor-pointer hover:bg-gray-100 p-2 rounded transition-colors"
onClick={toggleAddresses}
>
<h3 className="text-sm font-medium text-gray-900">Addresses ({listenAddresses.length}):</h3>
<button
type="button"
className="text-gray-500 hover:text-gray-700"
aria-expanded={addressesExpanded}
aria-label={addressesExpanded ? 'Collapse addresses' : 'Expand addresses'}
>
{addressesExpanded ? (
<ChevronUpIcon className="h-5 w-5" aria-hidden="true" />
) : (
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
)}
</button>
</div>
{addressesExpanded && (
<div className="mt-2 max-h-40 overflow-y-auto bg-white rounded border border-gray-200 p-2">
{listenAddresses.length === 0 ? (
<p className="p-2 text-sm text-gray-500 italic">No addresses available</p>
) : (
<ul className="divide-y divide-gray-100">
{listenAddresses.map((ma, index) => (
<li
className="text-xs text-gray-700 font-mono p-2 flex justify-between items-center hover:bg-gray-50"
key={`ma-${index}`}
>
<span className="break-all mr-2">{ma.toString()}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
copyAddress(index, ma.toString())
}}
className="flex-shrink-0 text-gray-400 hover:text-gray-600"
title="Copy address"
>
{copiedAddress === index ? (
<ClipboardDocumentCheckIcon className="h-4 w-4 text-green-500" />
) : (
<ClipboardIcon className="h-4 w-4" />
)}
</button>
</li>
))}
</ul>
)}
</div>
)}
{!addressesExpanded && listenAddresses.length > 0 && (
<p className="mt-2 text-xs text-gray-500 italic">Click to show {listenAddresses.length} addresses</p>
)}
</div>
<div className="bg-white p-4 rounded-lg border border-gray-200">
<label htmlFor="peer-id" className="block text-sm font-medium leading-6 text-gray-900">
Multiaddr to connect to
</label>
<div className="mt-2">
<input
value={maddr}
type="text"
name="peer-id"
id="peer-id"
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="12D3Koo..."
aria-describedby="multiaddr-id-description"
onChange={handleMultiaddrChange}
/>
</div>
<button
type="button"
className={
'rounded-md bg-indigo-600 mt-3 py-2 px-3 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600' +
(dialling ? ' cursor-not-allowed' : '')
}
onClick={handleConnectToMultiaddr}
disabled={dialling}
>
{dialling && <Spinner />} Connect{dialling && 'ing'} to multiaddr
</button>
{err && <p className="mt-2 text-sm text-red-500">{err}</p>}
</div>
{connections.length > 0 && (
<div className="bg-gray-50 p-4 rounded-lg">
<div
className="flex justify-between items-center cursor-pointer hover:bg-gray-100 p-2 rounded transition-colors"
onClick={toggleConnections}
>
<h3 className="text-sm font-medium text-gray-900">Connections ({connections.length}):</h3>
<button
type="button"
className="text-gray-500 hover:text-gray-700"
aria-expanded={connectionsExpanded}
aria-label={connectionsExpanded ? 'Collapse connections' : 'Expand connections'}
>
{connectionsExpanded ? (
<ChevronUpIcon className="h-5 w-5" aria-hidden="true" />
) : (
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
)}
</button>
</div>
{connectionsExpanded && (
<div className="mt-2 max-h-60 overflow-y-auto bg-white rounded border border-gray-200 p-2">
<PeerList connections={connections} />
</div>
)}
{!connectionsExpanded && (
<p className="mt-2 text-xs text-gray-500 italic">Click to show {connections.length} connections</p>
)}
</div>
)}
</div>
</DialogBody>
</Dialog>
)
}

View File

@@ -1,26 +1,17 @@
import { Fragment } from 'react'
import { Disclosure, Menu, Transition } from '@headlessui/react'
import { Disclosure, DisclosureButton, DisclosurePanel, Menu, Transition } from '@headlessui/react'
import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/react/24/outline'
import Link from 'next/link'
import Image from 'next/image'
import { useRouter } from 'next/router'
const navigationItems = [
{ name: 'Connecting to a Peer', href: '/' },
{ name: 'Chat', href: '/chat' },
{ name: 'Source', href: 'https://github.com/libp2p/universal-connectivity' },
]
const userNavigation = [
{ name: 'Your Profile', href: '#' },
{ name: 'Settings', href: '#' },
{ name: 'Sign out', href: '#' },
]
const navigationItems = [{ name: 'Source', href: 'https://github.com/libp2p/universal-connectivity' }]
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
}
export default function Navigation() {
export default function Navigation({ connectionInfoButton }: { connectionInfoButton?: React.ReactNode }) {
const router = useRouter()
return (
@@ -28,12 +19,25 @@ export default function Navigation() {
{({ open }) => (
<>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 justify-between">
<div className="flex">
<div className="flex h-16 justify-between items-center">
<div className="flex items-center">
<div className="flex flex-shrink-0 items-center">
<Image src="/libp2p-logo.svg" alt="libp2p logo" height="46" width="46" />
<div className="ml-3 flex items-center">
<h1 className="text-xl font-semibold text-gray-900 hidden sm:block">Universal Connectivity</h1>
<Image
src="/libp2p-hero.svg"
alt="libp2p hero"
height="24"
width="24"
className="ml-2 hidden sm:block"
/>
</div>
</div>
<div className="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
</div>
<div className="flex items-center space-x-4">
<div className="flex space-x-4">
{navigationItems.map((item) => (
<Link key={item.href} href={item.href} legacyBehavior>
<a
@@ -42,7 +46,7 @@ export default function Navigation() {
router.pathname === item.href
? 'border-indigo-500 text-gray-900'
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
'inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium',
'inline-flex items-center px-1 pt-1 text-sm font-medium',
)}
aria-current={router.pathname === item.href ? 'page' : undefined}
>
@@ -51,93 +55,10 @@ export default function Navigation() {
</Link>
))}
</div>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">
<button
type="button"
className="rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<span className="sr-only">View notifications</span>
<BellIcon className="h-6 w-6" aria-hidden="true" />
</button>
{/* Profile dropdown */}
<Menu as="div" className="relative ml-3">
<div>
{/* <Menu.Button className="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span className="sr-only">Open user menu</span>
<img
className="h-8 w-8 rounded-full"
src={user.imageUrl}
alt=""
/>
</Menu.Button> */}
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<a
href={item.href}
className={classNames(
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-700',
)}
>
{item.name}
</a>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</div>
<div className="-mr-2 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span className="sr-only">Open main menu</span>
{open ? (
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
<div className="flex items-center">{connectionInfoButton}</div>
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="space-y-1 pt-2 pb-3">
{navigationItems.map((item) => (
<Link key={item.href} href={item.href} legacyBehavior>
<Disclosure.Button
key={item.href}
// as="a"
// href={item.href}
className={classNames(
router.pathname === item.href
? 'border-indigo-500 bg-indigo-50 text-indigo-700'
: 'border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800',
'block border-l-4 py-2 pl-3 pr-4 text-base font-medium',
)}
aria-current={router.pathname === item.href ? 'page' : undefined}
>
{item.name}
</Disclosure.Button>
</Link>
))}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>

View File

@@ -41,7 +41,7 @@ function Peer({ connection }: PeerProps) {
return (
<li key={connection.id} className="flex justify-between gap-x-6 py-3">
<div className="flex min-w-0 gap-x-4">
<div className="flex min-w-0 gap-x-4 flex-grow">
<div className="mt-1 flex items-center gap-x-1.5">
<div className="flex-none rounded-full bg-emerald-500/20 p-1">
<div className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
@@ -59,14 +59,14 @@ function Peer({ connection }: PeerProps) {
</div>
</div>
{/* <div className="flex gap-x-2 items-center "> */}
<div className="hidden sm:flex sm:flex-col sm:items-end">
<div className="flex-shrink-0 ml-2">
<button
onClick={() => handleDisconnectPeer(connection.remotePeer)}
className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded flex flex-row"
className="bg-red-500 hover:bg-red-600 text-white rounded-full p-2 flex items-center justify-center"
title="Disconnect peer"
>
<XCircleIcon className="w-6 h-6" />
<span className="pl-1">Disconnect</span>
<XCircleIcon className="w-5 h-5" />
<span className="sr-only">Disconnect</span>
</button>
</div>
</li>

View File

@@ -1,24 +0,0 @@
import Head from 'next/head'
import Nav from '@/components/nav'
import ChatContainer from '@/components/chat'
export default function Chat() {
return (
<>
<Head>
<title>js-libp2p Chat</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="min-h-full">
<Nav />
<div className="">
<main>
<ChatContainer />
</main>
</div>
</main>
</>
)
}

View File

@@ -1,72 +1,17 @@
import Head from 'next/head'
import Nav from '@/components/nav'
import ChatContainer from '@/components/chat'
import ConnectionPanel from '@/components/connection-panel'
import { useState } from 'react'
import { useLibp2pContext } from '@/context/ctx'
import type { PeerUpdate, Connection } from '@libp2p/interface'
import { useCallback, useEffect, useState } from 'react'
import Image from 'next/image'
import { Multiaddr, multiaddr } from '@multiformats/multiaddr'
import { connectToMultiaddr } from '../lib/libp2p'
import Spinner from '@/components/spinner'
import PeerList from '@/components/peer-list'
import ConnectionInfoButton from '@/components/connection-info-button'
export default function Home() {
const { libp2p } = useLibp2pContext()
const [connections, setConnections] = useState<Connection[]>([])
const [listenAddresses, setListenAddresses] = useState<Multiaddr[]>([])
const [maddr, setMultiaddr] = useState('')
const [dialling, setDialling] = useState(false)
const [err, setErr] = useState('')
export default function Chat() {
const [isConnectionPanelOpen, setIsConnectionPanelOpen] = useState(false)
useEffect(() => {
const onConnection = () => {
const connections = libp2p.getConnections()
setConnections(connections)
}
onConnection()
libp2p.addEventListener('connection:open', onConnection)
libp2p.addEventListener('connection:close', onConnection)
return () => {
libp2p.removeEventListener('connection:open', onConnection)
libp2p.removeEventListener('connection:clone', onConnection)
}
}, [libp2p, setConnections])
useEffect(() => {
const onPeerUpdate = (evt: CustomEvent<PeerUpdate>) => {
const maddrs = evt.detail.peer.addresses?.map((p) => p.multiaddr)
setListenAddresses(maddrs ?? [])
}
libp2p.addEventListener('self:peer:update', onPeerUpdate)
return () => {
libp2p.removeEventListener('self:peer:update', onPeerUpdate)
}
}, [libp2p, setListenAddresses])
const handleConnectToMultiaddr = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>) => {
setErr('')
if (!maddr) {
return
}
setDialling(true)
try {
await connectToMultiaddr(libp2p)(multiaddr(maddr))
} catch (e: any) {
setErr(e?.message ?? 'Error connecting')
} finally {
setDialling(false)
}
},
[libp2p, maddr],
)
const handleMultiaddrChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setMultiaddr(e.target.value)
},
[setMultiaddr],
)
const handleOpenConnectionPanel = () => {
setIsConnectionPanelOpen(true)
}
return (
<>
@@ -76,71 +21,15 @@ export default function Home() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="min-h-full">
<Nav />
<div className="py-10">
<header>
<div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900 flex flex-row">
<p className="mr-4">Universal Connectivity</p>
<Image src="/libp2p-hero.svg" alt="libp2p logo" height="46" width="46" />
</h1>
</div>
</header>
<main>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<ul className="my-2 space-y-2 break-all">
<li className="">This PeerID: {libp2p.peerId.toString()}</li>
</ul>
Addresses:
<ul className="my-2 space-y-2 break-all">
{listenAddresses.map((ma, index) => (
<li className="text-xs text-gray-700" key={`ma-${index}`}>
{ma.toString()}
</li>
))}
</ul>
<div className="my-6 w-1/2">
<label htmlFor="peer-id" className="block text-sm font-medium leading-6 text-gray-900">
multiaddr to connect to
</label>
<div className="mt-2">
<input
value={maddr}
type="text"
name="peer-id"
id="peer-id"
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="12D3Koo..."
aria-describedby="multiaddr-id-description"
onChange={handleMultiaddrChange}
/>
</div>
<button
type="button"
className={
'rounded-md bg-indigo-600 my-2 py-2 px-3 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600' +
(dialling ? ' cursor-not-allowed' : '')
}
onClick={handleConnectToMultiaddr}
disabled={dialling}
>
{dialling && <Spinner />} Connect{dialling && 'ing'} to multiaddr
</button>
{err && <p className="text-red-500">{err}</p>}
</div>
<div>
{connections.length > 0 ? (
<>
<h3 className="text-xl">Connections ({connections.length}):</h3>
<PeerList connections={connections} />
</>
) : null}
</div>
</div>
</main>
<main className="min-h-full flex flex-col">
<Nav connectionInfoButton={<ConnectionInfoButton onClick={handleOpenConnectionPanel} />} />
<div className="flex-1 mx-auto w-full max-w-7xl px-0 sm:px-2 pt-0 pb-2 lg:px-8">
<div className="bg-white shadow-sm rounded-lg overflow-hidden border border-gray-200">
<ChatContainer />
</div>
</div>
</main>
<ConnectionPanel isOpen={isConnectionPanelOpen} onClose={() => setIsConnectionPanelOpen(false)} />
</>
)
}