Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: direct messages #175

Merged
merged 13 commits into from
Sep 17, 2024
6 changes: 5 additions & 1 deletion js-peer/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
},
"ignorePatterns": ["node_modules/", "build/", ".next/", "src/lib/protobuf/"]
}
2 changes: 2 additions & 0 deletions js-peer/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

certificates
2,508 changes: 1,568 additions & 940 deletions js-peer/package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions js-peer/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"name": "universal-connectivity-browser",
"scripts": {
"dev": "next dev",
"dev": "next dev --experimental-https",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"tsc": "tsc --noEmit -p tsconfig.json",
"protobuf": "protons src/lib/protobuf/*.proto"
},
"dependencies": {
"@chainsafe/libp2p-gossipsub": "^13.0.0",
Expand All @@ -28,8 +30,10 @@
"it-length-prefixed": "^9.0.4",
"it-map": "^3.1.0",
"it-pipe": "^3.0.1",
"it-protobuf-stream": "^1.1.4",
"libp2p": "^1.6.1",
"next": "14.2.3",
"protons-runtime": "^5.4.0",
"react": "18.3.1",
"react-18-blockies": "^1.0.6",
"react-dom": "18.3.1",
Expand All @@ -46,6 +50,7 @@
"eslint": "8.57.0",
"eslint-config-next": "14.2.3",
"postcss": "^8.4.38",
"protons": "^7.5.0",
"tailwindcss": "^3.4.3",
"typescript": "5.4.5"
}
Expand Down
32 changes: 10 additions & 22 deletions js-peer/src/components/chat-peer-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { useLibp2pContext } from '@/context/ctx'
import { CHAT_TOPIC } from '@/lib/constants'
import React, { useEffect, useState } from 'react'
import type { PeerId } from '@libp2p/interface'
import Blockies from 'react-18-blockies'
import { Peer } from './peer'
2color marked this conversation as resolved.
Show resolved Hide resolved

export function ChatPeerList() {
const { libp2p } = useLibp2pContext()
const [subscribers, setSubscribers] = useState<PeerId[]>([])

useEffect(() => {
const onSubscriptionChange = () => {
const subscribers = libp2p.services.pubsub.getSubscribers(CHAT_TOPIC)
const subscribers = libp2p.services.pubsub.getSubscribers(CHAT_TOPIC) as PeerId[]
setSubscribers(subscribers)
}
onSubscriptionChange()
Expand All @@ -23,28 +23,16 @@ 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>
<ul className="overflow-auto h-[32rem]">
{<Peer key={libp2p.peerId.toString()} peer={libp2p.peerId} self />}
<div className="overflow-auto h-[32rem]">
<div className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 focus:outline-none">
{<Peer peer={libp2p.peerId} self withName={true} withUnread={false} />}
</div>
{subscribers.map((p) => (
<Peer key={p.toString()} peer={p} self={false} />
<div key={p.toString()} className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 focus:outline-none">
<Peer peer={p} self={false} withName={true} withUnread={true} />
</div>
))}
</ul>
</div>
)
}

function Peer({ peer, self }: { peer: PeerId; self: boolean }) {
return (
<li className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 focus:outline-none">
<Blockies seed={peer.toString()} size={15} scale={3} className="rounded max-h-10 max-w-10" />
<div className="w-full pb-2">
<div className="flex justify-between">
<span className={`block ml-2 font-semibold ${self ? 'text-indigo-700-600' : 'text-gray-600'}`}>
{peer.toString().slice(-7)}
{self && ' (You)'}
</span>
</div>
</div>
</li>
</div>
)
}
173 changes: 150 additions & 23 deletions js-peer/src/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { useLibp2pContext } from '@/context/ctx'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { CHAT_FILE_TOPIC, CHAT_TOPIC, FILE_EXCHANGE_PROTOCOL } from '@/lib/constants'
import { CHAT_FILE_TOPIC, CHAT_TOPIC } from '@/lib/constants'
import { ChatFile, ChatMessage, useChatContext } from '../context/chat-ctx'
import { v4 as uuidv4 } from 'uuid'
import { MessageComponent } from './message'
import { Message } from './message'
import { forComponent } from '@/lib/logger'
import { ChatPeerList } from './chat-peer-list'
import { ChevronLeftIcon } from '@heroicons/react/20/solid'
import Blockies from 'react-18-blockies'
import { peerIdFromString } from '@libp2p/peer-id'

const log = forComponent('chat')

export const PUBLIC_CHAT_ROOM_ID = ''
const PUBLIC_CHAT_ROOM_NAME = 'Public Chat'

export default function ChatContainer() {
const { libp2p } = useLibp2pContext()
const { messageHistory, setMessageHistory, files, setFiles } = useChatContext()
const { roomId, setRoomId } = useChatContext()
const { messageHistory, setMessageHistory, directMessages, setDirectMessages, files, setFiles } = useChatContext()
const [input, setInput] = useState<string>('')
const fileRef = useRef<HTMLInputElement>(null)
const [messages, setMessages] = useState<ChatMessage[]>([])

const sendMessage = useCallback(async () => {
// Send message to public chat over gossipsub
const sendPublicMessage = useCallback(async () => {
if (input === '') return

log(
Expand All @@ -33,11 +42,58 @@ export default function ChatContainer() {

setMessageHistory([
...messageHistory,
{ msg: input, fileObjectUrl: undefined, from: 'me', peerId: myPeerId },
{
msgId: crypto.randomUUID(),
msg: input,
fileObjectUrl: undefined,
from: 'me',
peerId: myPeerId,
read: true,
receivedAt: Date.now(),
},
])

setInput('')
}, [input, messageHistory, setInput, libp2p, setMessageHistory])

// Send direct message over custom protocol
const sendDirectMessage = useCallback(async () => {
try {
const res = await libp2p.services.directMessage.send(peerIdFromString(roomId), input)

if (!res) {
log('Failed to send message')
return
}

const myPeerId = libp2p.peerId.toString()

const newMessage: ChatMessage = {
msgId: crypto.randomUUID(),
msg: input,
fileObjectUrl: undefined,
from: 'me',
peerId: myPeerId,
read: true,
receivedAt: Date.now(),
}

const updatedMessages = directMessages[roomId]
? [...directMessages[roomId], newMessage]
: [newMessage]

setDirectMessages({
...directMessages,
[roomId]: updatedMessages,
})

setInput('')
} catch (e: any) {
log(e)
}
}, [libp2p, setDirectMessages, directMessages, roomId, input])


const sendFile = useCallback(
async (readerEvent: ProgressEvent<FileReader>) => {
const fileBody = readerEvent.target?.result as ArrayBuffer
Expand All @@ -62,10 +118,13 @@ export default function ChatContainer() {
)

const msg: ChatMessage = {
msgId: crypto.randomUUID(),
msg: newChatFileMessage(file.id, file.body),
fileObjectUrl: window.URL.createObjectURL(new Blob([file.body])),
from: 'me',
peerId: myPeerId,
read: true,
receivedAt: Date.now(),
}
setMessageHistory([...messageHistory, msg])
},
Expand All @@ -81,16 +140,24 @@ export default function ChatContainer() {
if (e.key !== 'Enter') {
return
}
sendMessage()
if (roomId === PUBLIC_CHAT_ROOM_ID) {
sendPublicMessage()
} else {
sendDirectMessage()
}
},
[sendMessage],
[sendPublicMessage, sendDirectMessage, roomId],
)

const handleSend = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>) => {
sendMessage()
if (roomId === PUBLIC_CHAT_ROOM_ID) {
sendPublicMessage()
} else {
sendDirectMessage()
}
},
[sendMessage],
[sendPublicMessage, sendDirectMessage, roomId],
)

const handleInput = useCallback(
Expand Down Expand Up @@ -120,33 +187,93 @@ export default function ChatContainer() {
[fileRef],
)

const handleBackToPublic = () => {
setRoomId(PUBLIC_CHAT_ROOM_ID)
setMessages(messageHistory)
}

useEffect(() => {
// assumes a chat room is a peerId thus a direct message
if (roomId === PUBLIC_CHAT_ROOM_ID) {
setMessages(messageHistory)
} else {
setMessages(directMessages[roomId] || [])
}
}, [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="w-full">
<div className="relative flex items-center p-3 border-b border-gray-300">
<span className="block ml-2 font-bold text-gray-600">Public Chat</span>
{roomId === PUBLIC_CHAT_ROOM_ID &&
<span className="block ml-2 font-bold text-gray-600">{PUBLIC_CHAT_ROOM_NAME}</span>
}
{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>
<div className="relative w-full flex flex-col-reverse p-3 overflow-y-auto h-[40rem] bg-gray-100">
<ul className="space-y-2">
{/* messages start */}
{messageHistory.map(({ msg, fileObjectUrl, from, peerId }, idx) => (
<MessageComponent
key={idx}
msg={msg}
fileObjectUrl={fileObjectUrl}
from={from}
peerId={peerId}
/>
))}
{/* messages end */}
{messages.map(
({
msgId,
msg,
fileObjectUrl,
from,
peerId,
read,
receivedAt,
}: ChatMessage) => (
<Message
key={msgId}
dm={roomId !== ''}
msg={msg}
fileObjectUrl={fileObjectUrl}
from={from}
peerId={peerId}
read={read}
msgId={msgId}
receivedAt={receivedAt}
/>
),
)}
</ul>
</div>

<div className="flex items-center justify-between w-full p-3 border-t border-gray-300">
<input ref={fileRef} className="hidden" type="file" onChange={handleFileInput} />
<button onClick={handleFileSend}>
<input
ref={fileRef}
className="hidden"
type="file"
onChange={handleFileInput}
disabled={roomId !== PUBLIC_CHAT_ROOM_ID}
/>
<button
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'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5 text-gray-500"
Expand Down
Loading