URL := https://buga-chat.vercel.app/
REPO := https://github.com/kekda-py/buga-chat
BACKEND := https://github.com/kekda-py/buga-backend
So I was learning go
the other day. And was amazed by its channels. So decided to make something in it. Actually I thought making this app before I was learning go and wrote it half in python
but decided to make it go
cuz umm go is cool af.
also I made this thing before ===> dotenv
check it out
So umm my friend nexxel told me about this library fiber
its like quart
(async flask) for go. While browsing through their docs I found that you can make websockets with them. After that I literally scrapped the python code and started writing it in go
.
Websocket Hub
Since I was amazed by go channels. I used them to make a websocket hub. If u dont know how channels works. Here's a simple explanation.
Go Channels
Channels are a typed conduit through which you can send and receive values with the channel operator, <-
.
ch <- v // Send v to channel ch. v := <-ch // Receive from ch, and // assign value to v.
Like maps and slices, channels must be created before use: c := make(chan T)
Channels with Select
The select
statement lets a goroutine wait on multiple communication operations.
A select
blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.
tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") return default: fmt.Println(" .") time.Sleep(50 * time.Millisecond) } }
Output:
. . tick. . . tick. . . tick. . . tick. . . tick. BOOM!
I definitely didn't copied all this from Go Tour. What are you talking about?
Using this I made the websocket hub
At First I declared three channels
for communication between hub and the websocket and a map
for storing connections
var connections = make(map[*websocket.Conn]client) var register = make(chan *websocket.Conn) var broadcast = make(chan message) var unregister = make(chan *websocket.Conn)
and a message
struct for broadcasting
type struct message { content string by *websocket.Conn }
then in the Hub I made a select statement with the channels as the cases :-
for { select { case c := <- register {} case m := <- broadcast {} case c := <- unregister {} } }
<- register
just adds the connection to connections
case c := <-register: connections[c] = client{} log.Println("client registered")
<- broadcast
takes a type message
which has a by
attribute of type *websocket.Conn
. It loops through the connections
and checks if the user
is the one who sent the message. If it is then it just continue
s (skip to the next iteration). If its not then it sends the message.
The reason I made it like this. cuz if u send the message it was taking few seconds to appear. so in the frontend it adds the message instantly.
case m := <-broadcast: for c := range connections { if c == m.by { continue } if err := c.WriteMessage(websocket.TextMessage, []byte(m.content)); err != nil { log.Println("Error while sending message: ", err) c.WriteMessage(websocket.CloseMessage, []byte{}) c.Close() delete(connections, c) } }
<- unregister
just removes the connection
from connections
case c := <-unregister: delete(connections, c) log.Println("client unregistered")
now the websocket hub is done we just have to run it
go WebsocketHub()
now in the websocket we just have to register
and also defer unregister
register <- c defer func() { unregister <- c c.Close() }
and check for message
for { mt, m, err: = c.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Println("read error:", err) } return // Calls the deferred function, i.e. closes the connection on error } if mt == websocket.TextMessage { // MakeMessage(string(m), c) broadcast < -message { content: string(m), by: c, } } else { log.Println("websocket message received of type", mt) } }
now the backend
is done lets move to frontend
Frontend
I used Next.js
with chakra-ui
for this project.
for the websocket connection I used react-use-websocket
.
So first I added two states :-
const [messages, setMessages] = useState<Messages>({}); // ^^^ for the active messages const [msg, setMsg] = useState<string>(''); // ^^^ value of text in the message input
the Messages
interface is just
interface Messages { [key: string]: msg; }
and msg
:-
interface msg { byU : boolean; content : string; }
now time to run your backend
then add an environment variable NEXT_PUBLIC_BACKEND_URL
with your backend url to .env.local
. you can use
dotenv change NEXT_PUBLIC_BACKEND_URL the url --file .env.local
if u have dotenv
installed. then get that url by process.env.NEXT_PUBLIC_BACKEND_URL
and connect with it using
const { sendMessage, lastMessage, readyState} = useWebSocket(`wss://${BACKEND}/ws`, { shouldReconnect : (closeEvent) => true } );
make sure to import useWebsocket
along with ReadyState
import useWebSocket, { ReadyState } from 'react-use-websocket';
now connectionStatus
:-
const connectionStatus = { [ReadyState.CONNECTING]: 'Connecting', [ReadyState.OPEN]: 'Open', [ReadyState.CLOSING]: 'Closing', [ReadyState.CLOSED]: 'Closed', [ReadyState.UNINSTANTIATED]: 'Uninstantiated', }[readyState];
For messages, I looped through the keys using Object.keys
and used .map()
to render all of them.
{Object.keys(messages).map((key: string) => { if (messages[key] === undefined || messages[key] === null) return null; if (messages[key].content === undefined || messages[key].content === null) return null; return ( <Box key={key} borderRadius="lg" bg="teal" color="white" width="fit-content" px="5" py="2" ml={messages[key].byU ? "auto" : "0"} > {messages[key].content} </Box> ) } )}
if the message is sent by you. the marginLeft
is set to auto
which pushes it all the way to right side.
now time for checking for messages. we just use a useEffect
hook with lastMessage
as dependency.
useEffect(() => { if (lastMessage !== undefined || lastMessage !== null) { (function (m: string) { setMessages((prev: Messages) => { let id = getUID(); while (prev[id] !== undefined || prev[id] !== undefined) { id = getUID(); } setTimeout(() => { deleteMessage(id); }, 1000 * 60); return { ...prev, [id]: { byU: false, content: m, }, }; }); if (mute) return; new Audio("ping.mp3").play(); })(lastMessage?.data); } }, [lastMessage]);
I am using Date.now()
for the ids. and also setting a timeout
for 1 min which runs the deleteMessage
function :-
function deleteMessage(id: string) { setMessages((prev) => { const newMessages = { ...prev }; delete newMessages[id]; return newMessages; }); }
now for sending messages we create another function which just sends the message using sendMessage
which we got from useWebsocket
hook :-
function Send() { if ( msg.length < 1 || connectionStatus !== "Open" || msg === undefined || msg === null ) return; sendMessage(msg); newMessage(msg, true); setMsg(""); }
and on Enter
we run it
onKeyUp={(e : any) => { if (e.key === "Enter") { Send() } }}
this is a prop on the input element.
and now there you go u made a completely Anonymous chat app.
run
yarn dev
to run the app in development mode
Top comments (0)