diff --git a/js-peer/src/components/chat-peer-list.tsx b/js-peer/src/components/chat-peer-list.tsx index ed76f77..4835560 100644 --- a/js-peer/src/components/chat-peer-list.tsx +++ b/js-peer/src/components/chat-peer-list.tsx @@ -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([]) @@ -22,8 +26,8 @@ export function ChatPeerList() { return (
-

Peers

-
+ {!hideHeader &&

Peers

} +
{}
diff --git a/js-peer/src/components/chat.tsx b/js-peer/src/components/chat.tsx index 602f388..ba7a97a 100644 --- a/js-peer/src/components/chat.tsx +++ b/js-peer/src/components/chat.tsx @@ -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('') const fileRef = useRef(null) const [messages, setMessages] = useState([]) + 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 ( -
-
-
+
+
+
{roomId === PUBLIC_CHAT_ROOM_ID && ( - {PUBLIC_CHAT_ROOM_NAME} + <> + {PUBLIC_CHAT_ROOM_NAME} + + )} {roomId !== PUBLIC_CHAT_ROOM_ID && ( <> {roomId.toString().slice(-7)} - +
+ + +
)}
-
+ + {/* Show mobile peer list when toggled */} + {showMobilePeerList && ( +
+
+

Peers

+ +
+ +
+ )} + +
    {messages.map(({ msgId, msg, fileObjectUrl, peerId, read, receivedAt }: ChatMessage) => (
-
+
@@ -282,7 +333,9 @@ export default function ChatContainer() {
- +
+ +
) diff --git a/js-peer/src/components/connection-info-button.tsx b/js-peer/src/components/connection-info-button.tsx new file mode 100644 index 0000000..5cff4a0 --- /dev/null +++ b/js-peer/src/components/connection-info-button.tsx @@ -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 ( + + ) +} diff --git a/js-peer/src/components/connection-panel.tsx b/js-peer/src/components/connection-panel.tsx new file mode 100644 index 0000000..af79873 --- /dev/null +++ b/js-peer/src/components/connection-panel.tsx @@ -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([]) + const [listenAddresses, setListenAddresses] = useState([]) + 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(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) => { + 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) => { + 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) => { + 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 ( + +
+ Connection Information + +
+ +
+
+

This PeerID:

+
+

{libp2p.peerId.toString()}

+ +
+
+ +
+
+

Addresses ({listenAddresses.length}):

+ +
+ {addressesExpanded && ( +
+ {listenAddresses.length === 0 ? ( +

No addresses available

+ ) : ( +
    + {listenAddresses.map((ma, index) => ( +
  • + {ma.toString()} + +
  • + ))} +
+ )} +
+ )} + {!addressesExpanded && listenAddresses.length > 0 && ( +

Click to show {listenAddresses.length} addresses

+ )} +
+ +
+ +
+ +
+ + {err &&

{err}

} +
+ + {connections.length > 0 && ( +
+
+

Connections ({connections.length}):

+ +
+ {connectionsExpanded && ( +
+ +
+ )} + {!connectionsExpanded && ( +

Click to show {connections.length} connections

+ )} +
+ )} +
+
+
+ ) +} diff --git a/js-peer/src/components/nav.tsx b/js-peer/src/components/nav.tsx index 8441dcc..e4e221f 100644 --- a/js-peer/src/components/nav.tsx +++ b/js-peer/src/components/nav.tsx @@ -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 }) => ( <>
-
-
+
+
libp2p logo +
+

Universal Connectivity

+ libp2p hero +
-
+
+ + - -
- {/* Mobile menu button */} - - Open main menu - {open ? ( - +
{connectionInfoButton}
- - -
- {navigationItems.map((item) => ( - - - {item.name} - - - ))} -
-
)} diff --git a/js-peer/src/components/peer-list.tsx b/js-peer/src/components/peer-list.tsx index 05342b8..cdd5deb 100644 --- a/js-peer/src/components/peer-list.tsx +++ b/js-peer/src/components/peer-list.tsx @@ -41,7 +41,7 @@ function Peer({ connection }: PeerProps) { return (
  • -
    +
    @@ -59,14 +59,14 @@ function Peer({ connection }: PeerProps) {
    - {/*
    */} -
    +
  • diff --git a/js-peer/src/pages/chat.tsx b/js-peer/src/pages/chat.tsx deleted file mode 100644 index ef7bfec..0000000 --- a/js-peer/src/pages/chat.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import Head from 'next/head' -import Nav from '@/components/nav' -import ChatContainer from '@/components/chat' - -export default function Chat() { - return ( - <> - - js-libp2p Chat - - - - -
    -
    - - ) -} diff --git a/js-peer/src/pages/index.tsx b/js-peer/src/pages/index.tsx index 478842e..2f9a0db 100644 --- a/js-peer/src/pages/index.tsx +++ b/js-peer/src/pages/index.tsx @@ -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([]) - const [listenAddresses, setListenAddresses] = useState([]) - 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) => { - 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) => { - 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) => { - setMultiaddr(e.target.value) - }, - [setMultiaddr], - ) + const handleOpenConnectionPanel = () => { + setIsConnectionPanelOpen(true) + } return ( <> @@ -76,71 +21,15 @@ export default function Home() { -
    -