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)