DEV Community

Yash
Yash

Posted on

How I made a completely Anonymous chat app with go and next.js

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.
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

Output:

    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
BOOM!
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

and a message struct for broadcasting

type struct message {
  content string
  by      *websocket.Conn
}
Enter fullscreen mode Exit fullscreen mode

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 {}
  }
}
Enter fullscreen mode Exit fullscreen mode

<- register just adds the connection to connections

case c := <-register:
  connections[c] = client{}
  log.Println("client registered")
Enter fullscreen mode Exit fullscreen mode

<- 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 continues (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)
    }
  }
Enter fullscreen mode Exit fullscreen mode

<- unregister just removes the connection from connections

case c := <-unregister:
  delete(connections, c)
  log.Println("client unregistered")
Enter fullscreen mode Exit fullscreen mode

now the websocket hub is done we just have to run it

go WebsocketHub()
Enter fullscreen mode Exit fullscreen mode

now in the websocket we just have to register and also defer unregister

register <- c

defer func() {
  unregister <- c
  c.Close()
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

the Messages interface is just

interface Messages {
  [key: string]: msg;
}
Enter fullscreen mode Exit fullscreen mode

and msg :-

interface msg {
  byU : boolean;
  content : string;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode


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 } );
Enter fullscreen mode Exit fullscreen mode

make sure to import useWebsocket along with ReadyState

import useWebSocket, { ReadyState } from 'react-use-websocket';
Enter fullscreen mode Exit fullscreen mode

now connectionStatus :-

const connectionStatus = {
  [ReadyState.CONNECTING]: 'Connecting',
  [ReadyState.OPEN]: 'Open',
  [ReadyState.CLOSING]: 'Closing',
  [ReadyState.CLOSED]: 'Closed',
  [ReadyState.UNINSTANTIATED]: 'Uninstantiated',
}[readyState];
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
)}
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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("");
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

to run the app in development mode


Buga-Chat

Discussion (0)