DEV Community

Cover image for Multi-room Chat Application With WebSockets In Go And Vue.js (Part 2)
Jeroen de Kok
Jeroen de Kok

Posted on • Originally published at whichdev.com on

Multi-room Chat Application With WebSockets In Go And Vue.js (Part 2)

In this tutorial series, we will build a fully working chat application, the server part will be built with WebSockets in Go, on the front-end we will leverage Vue.js to create a simple interface.

This is the second part of this series, in part 1 we completed the basic chat application with one chat room. In this part, we will introduce multi-room support and allow users to chat privately in a one on one room.

Posts overview

  • Basic chat application with Go + Vue.JS
  • Multi-room & 1 one 1 chats. (This page)
  • Using Redis Pub/Sub for scalability (Coming soon!)
  • Adding authentication and allow users to log-in (Coming soon!)

Preconditions

To follow along you should have completed part 1 or grab the source from here.

Step 1: Introducing rooms

The first thing we will do is to create a new struct for our new Room type. The room struct looks a lot like how the ChatServer is currently structured. Because each room should be able to register clients, unregister clients, and broadcast to clients, we will add the same maps and channels as in the ChatServer.

//room.go
package main

import "fmt"

type Room struct {
    name       string
    clients    map[*Client]bool
    register   chan *Client
    unregister chan *Client
    broadcast  chan *Message
}

// NewRoom creates a new Room
func NewRoom(name string) *Room {
    return &Room{
        name:       name,
        clients:    make(map[*Client]bool),
        register:   make(chan *Client),
        unregister: make(chan *Client),
        broadcast:  make(chan *Message),
    }
}

// RunRoom runs our room, accepting various requests
func (room *Room) RunRoom() {
    for {
        select {

        case client := <-room.register:
            room.registerClientInRoom(client)

        case client := <-room.unregister:
            room.unregisterClientInRoom(client)

        case message := <-room.broadcast:
            room.broadcastToClientsInRoom(message)
        }
    }
}

func (room *Room) registerClientInRoom(client *Client) {
    room.notifyClientJoined(client)
    room.clients[client] = true
}

func (room *Room) unregisterClientInRoom(client *Client) {
    if _, ok := room.clients[client]; ok {
        delete(room.clients, client)
    }
}

func (room *Room) broadcastToClientsInRoom(message []byte) {
    for client := range room.clients {
        client.send <- message
    }
}
Enter fullscreen mode Exit fullscreen mode

The only new thing here is the Name property, this is to identify each room later on.

Keeping track of the rooms

Because our ChatServer acts like a hub for connecting the parts of our chat application, we will use it to keep track of all the rooms that will be created.

First add a new map to hold the rooms:

//chatServer.go
type WsServer struct {
    ...
    rooms      map[*Room]bool
}

// NewWebsocketServer creates a new WsServer type
func NewWebsocketServer() *WsServer {
    return &WsServer{
        ...
        rooms:      make(map[*Room]bool),
    }
}
Enter fullscreen mode Exit fullscreen mode

We will keep the existing maps and channels for now, me might need them when we want to know who is online.

Next add a method to find a existing room and to create a new one:

//chatServer.go
func (server *WsServer) findRoomByName(name string) *Room {
    var foundRoom *Room
    for room := range server.rooms {
        if room.GetName() == name {
            foundRoom = room
            break
        }
    }

    return foundRoom
}

func (server *WsServer) createRoom(name string) *Room {
    room := NewRoom(name)
    go room.RunRoom()
    server.rooms[room] = true

    return room
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Changing the Client to accept various requests

In this next step, we will update our client, so it is able to handle a wider variety of requests than just passing along a text message to the ChatServer.

Message types

To handle different kinds of messages, like joining a room or sending a chat message, we will introduce a Message type. This type will consist of:

  • Action: What action is the message requesting (send-message, join-room or leave-room)?
  • Message: The actual message, could be a room name when joining a room for example.
  • Target: A target for the message, a room for example.
  • Sender: Who is sending the message?
//message.go
package main

import (
    "encoding/json"
    "log"
)

const SendMessageAction = "send-message"
const JoinRoomAction = "join-room"
const LeaveRoomAction = "leave-room"

type Message struct {
    Action  string  `json:"action"`
    Message string  `json:"message"`
    Target  string  `json:"target"`
    Sender  *Client `json:"sender"`
}

func (message *Message) encode() []byte {
    json, err := json.Marshal(message)
    if err != nil {
        log.Println(err)
    }

    return json
}
Enter fullscreen mode Exit fullscreen mode

The JSON tags are needed for encoding and decoding, see the documentation for more info.

We will include an encode method that can be called to create a json []byte object that is ready to be send back to clients.

Interacting with the rooms

Now it’s time to modify our Client so it can join, leave, and broadcast to rooms. First, we will add a map to our client this is for keeping track of the rooms this client joins, in case he or she wants to leave one.

We will also modify the disconnect method so a client will be unregistered from each room next to unregistering from the ChatServer.

//client.go
type Client struct {
    ...
    rooms    map[*Room]bool
}

func newClient(conn *websocket.Conn, wsServer *WsServer) *Client {
    return &Client{
        ...
        rooms:    make(map[*Room]bool),
    }
}

func (client *Client) disconnect() {
    client.wsServer.unregister <- client
    for room := range client.rooms {
        room.unregister <- client
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Handling messages

Now we are able to actually join a room. As mentioned earlier we created a Message type with different actions. Lets now use those actions to handle the different kinds of messages.

First, modify the client and add a new method that decodes the JSON message and then handles it directly or passes it to a dedicated handler:

// client.go
import (
    "encoding/json"
    ...
)

func (client *Client) handleNewMessage(jsonMessage []byte) {

    var message Message
    if err := json.Unmarshal(jsonMessage, &message); err != nil {
        log.Printf("Error on unmarshal JSON message %s", err)
    }

    // Attach the client object as the sender of the messsage.
    message.Sender = client

    switch message.Action {
    case SendMessageAction:
        // The send-message action, this will send messages to a specific room now.
        // Which room wil depend on the message Target
        roomName := message.Target
        // Use the ChatServer method to find the room, and if found, broadcast!
        if room := client.wsServer.findRoomByName(roomName); room != nil {
            room.broadcast <- &message
        }
    // We delegate the join and leave actions. 
    case JoinRoomAction:
        client.handleJoinRoomMessage(message)

    case LeaveRoomAction:
        client.handleLeaveRoomMessage(message)
    }
}
Enter fullscreen mode Exit fullscreen mode

So with the new method above, we will send messages directly to a room now! Since we are sending Message objects now instead of []byte objects, we will need to make a minor adjustment to our room.go.

// room.go
func (room *Room) RunRoom() {
    for {
        select {
        ...
        case message := <-room.broadcast:
            room.broadcastToClientsInRoom(message.encode())
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

The methods for joining and leaving a room are fairly simple. When the room does not exist yet we will create one:

// client.go
func (client *Client) handleJoinRoomMessage(message Message) {
    roomName := message.Message

    room := client.wsServer.findRoomByName(roomName)
    if room == nil {
        room = client.wsServer.createRoom(roomName)
    }

    client.rooms[room] = true

    room.register <- client
}

func (client *Client) handleLeaveRoomMessage(message Message) {
    room := client.wsServer.findRoomByName(message.Message)
    if _, ok := client.rooms[room]; ok {
        delete(client.rooms, room)
    }

    room.unregister <- client
}
Enter fullscreen mode Exit fullscreen mode

upon leaving a room we both remove it from the client and unregister the client from the room.

The last step here is to use our new methods when a new message is received so switch out the first rule for the second in client.go :

- client.wsServer.broadcast <- jsonMessage
+ client.handleNewMessage(jsonMessage)
Enter fullscreen mode Exit fullscreen mode

Naming our client

It would be nice to see whom you are chatting with, so let’s give our so-far anonymous client a name.

// client.go
type Client struct {
    ... 
    Name     string `json:"name"`   
}

func newClient(conn *websocket.Conn, wsServer *WsServer, name string) *Client {
    return &Client{
        Name:     name,
        ...
    }
}

func (client *Client) GetName() string {
    return client.Name
}
Enter fullscreen mode Exit fullscreen mode

Notice the json tags again, the client is used as sender in the message type. Now it wil be encoded with the name of the sender.

To get the name form the user, we will allow the use of a parameter on the connection string. For this we have to modify the existing ServeWs method

// client.go
func ServeWs(wsServer *WsServer, w http.ResponseWriter, r *http.Request) {

    name, ok := r.URL.Query()["name"]

    if !ok || len(name[0]) < 1 {
        log.Println("Url Param 'name' is missing")
        return
    }

    ...

    client := newClient(conn, wsServer, name[0])

    ...
}
Enter fullscreen mode Exit fullscreen mode

Welcome message

The last thing we will do is send out a message when a user joins a room, so other users see him or her joining!

For this we will add one new method in room.go

// room.go
const welcomeMessage = "%s joined the room"

func (room *Room) notifyClientJoined(client *Client) {
    message := &Message{
        Action:  SendMessageAction,
        Target:  room.name,
        Message: fmt.Sprintf(welcomeMessage, client.GetName()),
    }

    room.broadcastToClientsInRoom(message.encode())
}
Enter fullscreen mode Exit fullscreen mode

Then call this method in when a user registers:

// room.go
func (room *Room) registerClientInRoom(client *Client) {
    // by sending the message first the new user won't see his own message.
    room.notifyClientJoined(client)
    room.clients[client] = true
}
Enter fullscreen mode Exit fullscreen mode

That’s all the server code for our public chat rooms. Now let’s update our front-end code!

Step 3: Room interface

Let’s start out with the JavaScript. To keep track of our new components (rooms & the name of a user) we will drop a few data properties and add some other. The final result should look like:

// public/assets/app.js
var app = new Vue({
  el: '#app',
  data: {
    ws: null,
    serverUrl: "ws://localhost:8080/ws",
    roomInput: null,
    rooms: [],
    user: {
      name: ""
    }
  },
  ... 
})
Enter fullscreen mode Exit fullscreen mode
  • roomInput is used for joining new rooms.
  • rooms wil keep track of the joined rooms.
  • user is for user data, like the name.

The modified methods look as followed:

// public/assets/app.js
methods: {
    connect() {
        this.connectToWebsocket();
    },
    connectToWebsocket() {
        // Pass the name paramter when connecting.
        this.ws = new WebSocket(this.serverUrl + "?name=" + this.user.name);
        this.ws.addEventListener('open', (event) => { this.onWebsocketOpen(event) });
        this.ws.addEventListener('message', (event) => { this.handleNewMessage(event) });
    },
    onWebsocketOpen() {
        console.log("connected to WS!");
    },

    handleNewMessage(event) {
        let data = event.data;
        data = data.split(/\r?\n/);

        for (let i = 0; i < data.length; i++) {
        let msg = JSON.parse(data[i]);
        // display the message in the correct room.
        const room = this.findRoom(msg.target);
        if (typeof room !== "undefined") {
            room.messages.push(msg);
        }
        }
    },
    sendMessage(room) {
        // send message to correct room.
        if (room.newMessage !== "") {
        this.ws.send(JSON.stringify({
            action: 'send-message',
            message: room.newMessage,
            target: room.name
        }));
        room.newMessage = "";
        }
    },
    findRoom(roomName) {
        for (let i = 0; i < this.rooms.length; i++) {
          if (this.rooms[i].name === roomName) {
              return this.rooms[i];
          }
        }
    },
    joinRoom() {
        this.ws.send(JSON.stringify({ action: 'join-room', message: this.roomInput }));
        this.messages = [];
        this.rooms.push({ "name": this.roomInput, "messages": [] });
        this.roomInput = "";
    },
    leaveRoom(room) {
        this.ws.send(JSON.stringify({ action: 'leave-room', message: room.name }));

        for (let i = 0; i < this.rooms.length; i++) {
          if (this.rooms[i].name === room.name) {
              this.rooms.splice(i, 1);
              break;
          }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

we’ve introduced three new methods for finding a room, joining a room and leaving a room.

The existing receive and send methods are modified to work with the rooms.

The HTML is modified to include an input for choosing a name and for joining a room. After filling out a name it will connect to the WebSocket server.

<!-- public/index.html-->
...
  <body>
    <div id="app">
      <div class="container h-100">
        <div class="row justify-content-center h-100">
          <div class="col-12 form" v-if="!ws">
              <div class="input-group">
                <input
                  v-model="user.name"
                  class="form-control name"
                  placeholder="Please fill in your (nick)name"
                  @keyup.enter.exact="connect"
                />
                <div class="input-group-append">
                  <span class="input-group-text send_btn" @click="connect">
                  >
                  </span>
                </div>
            </div>
          </div>

          <div class="col-12 room" v-if="ws != null">
            <div class="input-group">
              <input
                v-model="roomInput"
                class="form-control name"
                placeholder="Type the room you want to join"
                @keyup.enter.exact="joinRoom"
              />
              <div class="input-group-append">
                <span class="input-group-text send_btn" @click="joinRoom">
                >
                </span>
              </div>
            </div>
          </div>

          <div class="chat" v-for="(room, key) in rooms" :key="key">
            <div class="card">
              <div class="card-header msg_head">
                <div class="d-flex bd-highlight justify-content-center">
                  {{room.name}}
                  <span class="card-close" @click="leaveRoom(room)">leave</span>
                </div>
              </div>
              <div class="card-body msg_card_body">
                <div
                  v-for="(message, key) in room.messages"
                  :key="key"
                  class="d-flex justify-content-start mb-4"
                >
                  <div class="msg_cotainer">
                    {{message.message}}
                    <span class="msg_name" v-if="message.sender">{{message.sender.name}}</span>
                  </div>
                </div>
              </div>
              <div class="card-footer">
                <div class="input-group">
                  <textarea
                    v-model="room.newMessage"
                    name=""
                    class="form-control type_msg"
                    placeholder="Type your message..."
                    @keyup.enter.exact="sendMessage(room)"
                  ></textarea>
                  <div class="input-group-append">
                    <span class="input-group-text send_btn" @click="sendMessage(room)"
                      >></span
                    >
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
Enter fullscreen mode Exit fullscreen mode

The modified style can be found here: https://github.com/jeroendk/go-vuejs-chat/blob/master/public/assets/style.css

That is it for public rooms! We should now be able to run our code… Start your server with:

go run ./
Enter fullscreen mode Exit fullscreen mode

And visit: http://localhost:8080

You should be able to join multiple rooms and start chatting.

Demo chatting

Step 4: Listing online users

Not that we can join chat rooms on the fly it is time to add one on one chats. We will make use of the existing room architecture to start private chats.

To be able to start a private chat, the user should be able to see who is online. So the first thing we will do is notify users when a user joins or leaves.

Let’s open our Message file and add two more actions:

// message.go
const UserJoinedAction = "user-join"
const UserLeftAction = "user-left"
Enter fullscreen mode Exit fullscreen mode

Identify users

To keep users apart let’s add a unique ID to our user struct. For this we will use the UUID bundle, install it as follows:

go get github.com/google/uuid
Enter fullscreen mode Exit fullscreen mode

After installing modify the Client struct to include a field for the ID and generate a new ID on creation:

// client.go
import (
    ...
    "github.com/google/uuid"
)
type Client struct {
    ...
    ID       uuid.UUID `json:"id"`
    ...
}

func newClient(conn *websocket.Conn, wsServer *WsServer, name string) *Client {
    return &Client{
        ID:       uuid.New(),
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Sending notifications

Next, we will implement the methods for notifying the users, this will be done in the chatServer because that is were users are joining and leaving.

We will add three methods:

  • notifyClientJoined, to notify existing users when a new one joins.
  • notifyClientLeft, to notify existing users when one leaves.
  • listOnlineClients, to list all the existing users to the joining user.
// chatServer.go
func (server *WsServer) notifyClientJoined(client *Client) {
    message := &Message{
        Action: UserJoinedAction,
        Sender: client,
    }

    server.broadcastToClients(message.encode())
}

func (server *WsServer) notifyClientLeft(client *Client) {
    message := &Message{
        Action: UserLeftAction,
        Sender: client,
    }

    server.broadcastToClients(message.encode())
}

func (server *WsServer) listOnlineClients(client *Client) {
    for existingClient := range server.clients {
        message := &Message{
            Action: UserJoinedAction,
            Sender: existingClient,
        }
        client.send <- message.encode()
    }
}
Enter fullscreen mode Exit fullscreen mode

At last we have to call these methods at the appropriate times.

The existing register and unregister methods will now look like this:

// chatServer.go
func (server *WsServer) registerClient(client *Client) {
    server.notifyClientJoined(client)
    server.listOnlineClients(client)
    server.clients[client] = true
}

func (server *WsServer) unregisterClient(client *Client) {
    if _, ok := server.clients[client]; ok {
        delete(server.clients, client)
        server.notifyClientLeft(client)
    }
}
Enter fullscreen mode Exit fullscreen mode

Notify all the clients of the new client and list all the clients to the new one before we add the new client to the map. This way the client won’t see his own profile.

Handling the notifications

Now we should be able to listen to the notifications in the JavaScript. First, we will add a data property and the methods for handling these notifications:

// public/assets/app.js
data: {
    ...
    users: []
},

handleUserJoined(msg) {
    this.users.push(msg.sender);
},
handleUserLeft(msg) {
    for (let i = 0; i < this.users.length; i++) {
        if (this.users[i].id == msg.sender.id) {
            this.users.splice(i, 1);
        }
    }
},
Enter fullscreen mode Exit fullscreen mode

Next refactor the handleNewMessage method so it can act upon multiple types of messages.

// public/assets/app.js
handleNewMessage(event) {
    let data = event.data;
    data = data.split(/\r?\n/);

    for (let i = 0; i < data.length; i++) {
        let msg = JSON.parse(data[i]);
        switch (msg.action) {
        case "send-message":
            this.handleChatMessage(msg);
            break;
        case "user-join":
            this.handleUserJoined(msg);
            break;
        case "user-left":
            this.handleUserLeft(msg);
            break;
        default:
            break;
        }
    }
},

handleChatMessage(msg) {
  const room = this.findRoom(msg.target);
  if (typeof room !== "undefined") {
    room.messages.push(msg);
  }
},
Enter fullscreen mode Exit fullscreen mode

Listing the users

Alright, it is time to display our online users, open the HTML file, and add the following just above:

<!-- public/index.html-->
<div class="col-12 ">
    <div class="row">
        <div class="col-2 card profile" v-for="user in users" :key="user.id">
        <div class="card-header">{{user.name}}</div>
        <div class="card-body">
            <button class="btn btn-primary">Send Message</button>
        </div>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

You can add the above code just before the room input field:

<div class="col-12 room" v-if="ws != null">
Enter fullscreen mode Exit fullscreen mode

The modified style can be found here: https://github.com/jeroendk/go-vuejs-chat/blob/master/public/assets/style.css

Now join the server with multiple tabs and the result should look like this:

Step 5: Starting a private chat

We are now able to see who is online, so the next step is to create a private chat room when you want to message someone.

First open up the message file again, we will add two more actions:

  • JoinRoomPrivateAction, to join or create a private room.
  • RoomJoinedAction, to send back to the user after joining a room. Soon will identify rooms by ID instead of the name so we have to let our users know what this ID is.
// message.go
const JoinRoomPrivateAction = "join-room-private"
const RoomJoinedAction = "room-joined"


type Message struct {
-   Target  string  `json:"target"` 
+   Target  *Room   `json:"target"`
}
Enter fullscreen mode Exit fullscreen mode

We changed the target in a message to the Room type instead of a string, this way we can include the ID and the name.

Identify Rooms

Now let’s give our rooms an ID like with the users we will use the UUID library. We will expose the Name field in JSON as well by making it public (capitalize) and adding the JSON tags. At last, we add a boolean field that indicates if the room is private or public.

// room.go
import (
    ...
    "github.com/google/uuid"
)

type Room struct {
    ID         uuid.UUID `json:"id"`
    Name       string    `json:"name"`  
    Private    bool `json:"private"`
    ...
}

func NewRoom(name string, private bool) *Room {
    return &Room{
        ID:         uuid.New(),
        Name:       name,       
        Private:    private,
        ...
    }
}

func (room *Room) GetId() string {
    return room.ID.String()
}

func (room *Room) GetName() string {
    return room.Name
}
Enter fullscreen mode Exit fullscreen mode

To comply with the changed message struct we make one more adjustment to the room file. In the notifyClientJoined method change the Target to room instead of room.name.

// room.go
message := &Message{
  Action:  SendMessageAction,
  Target:  room,
  Message: fmt.Sprintf(welcomeMessage, client.GetName()),
}
Enter fullscreen mode Exit fullscreen mode

If you want to suppress the welcome message when another user joins the private room, change the registerClientInRoom method like this:

// room.go
func (room *Room) registerClientInRoom(client *Client) {
    if !room.Private {
        room.notifyClientJoined(client)
    }
    room.clients[client] = true
}
Enter fullscreen mode Exit fullscreen mode

Joining a private room

To join a private room we can reuse a lot of existing code in the client file, therefor we refactor it to use it both for private and public rooms. The end result looks like below. See the inline comments for some more explanation.

// client.go

// Refactored method
// Use the ID of the target room instead of the name to find it.
// Added case for joining private room
func (client *Client) handleNewMessage(jsonMessage []byte) {

    var message Message
    if err := json.Unmarshal(jsonMessage, &message); err != nil {
        log.Printf("Error on unmarshal JSON message %s", err)
        return
    }

    message.Sender = client

    switch message.Action {
    case SendMessageAction:
        roomID := message.Target.GetId()
        if room := client.wsServer.findRoomByID(roomID); room != nil {
            room.broadcast <- &message
        }

    case JoinRoomAction:
        client.handleJoinRoomMessage(message)

    case LeaveRoomAction:
        client.handleLeaveRoomMessage(message)

    case JoinRoomPrivateAction:
        client.handleJoinRoomPrivateMessage(message)
    }

}

// Refactored method
// Use new joinRoom method
func (client *Client) handleJoinRoomMessage(message Message) {
    roomName := message.Message

    client.joinRoom(roomName, nil)
}

// Refactored method
// Added nil check
func (client *Client) handleLeaveRoomMessage(message Message) {
    room := client.wsServer.findRoomByID(message.Message)
    if room == nil {
        return
    }
    if _, ok := client.rooms[room]; ok {
        delete(client.rooms, room)
    }

    room.unregister <- client
}

// New method
// When joining a private room we will combine the IDs of the users
// Then we will bothe join the client and the target.
func (client *Client) handleJoinRoomPrivateMessage(message Message) {

    target := client.wsServer.findClientByID(message.Message)
    if target == nil {
        return
    }

    // create unique room name combined to the two IDs
    roomName := message.Message + client.ID.String()

    client.joinRoom(roomName, target)
    target.joinRoom(roomName, client)

}

// New method
// Joining a room both for public and private roooms
// When joiing a private room a sender is passed as the opposing party
func (client *Client) joinRoom(roomName string, sender *Client) {

    room := client.wsServer.findRoomByName(roomName)
    if room == nil {
        room = client.wsServer.createRoom(roomName, sender != nil)
    }

    // Don't allow to join private rooms through public room message
    if sender == nil && room.Private {
        return
    }

    if !client.isInRoom(room) {
        client.rooms[room] = true
        room.register <- client
        client.notifyRoomJoined(room, sender)
    }

}

// New method
// Check if the client is not yet in the room
func (client *Client) isInRoom(room *Room) bool {
    if _, ok := client.rooms[room]; ok {
        return true
    }
    return false
}

// New method
// Notify the client of the new room he/she joined
func (client *Client) notifyRoomJoined(room *Room, sender *Client) {
    message := Message{
        Action: RoomJoinedAction,
        Target: room,
        Sender: sender,
    }

    client.send <- message.encode()
}
Enter fullscreen mode Exit fullscreen mode

After applying the above, we need to update our chatServer and add two new methods used by the client:

// chatServer.go
func (server *WsServer) findRoomByID(ID string) *Room {
    var foundRoom *Room
    for room := range server.rooms {
        if room.GetId() == ID {
            foundRoom = room
            break
        }
    }

    return foundRoom
}

func (server *WsServer) findClientByID(ID string) *Client {
    var foundClient *Client
    for client := range server.clients {
        if client.ID.String() == ID {
            foundClient = client
            break
        }
    }

    return foundClient
}
Enter fullscreen mode Exit fullscreen mode

And at last change the joinRoom method to pass the private boolean:

// chatServer.go
func (server *WsServer) createRoom(name string, private bool) *Room {
    room := NewRoom(name, private)
    ...
}
Enter fullscreen mode Exit fullscreen mode

Putting it al together

Alright, it is time to let it all fall in place by finishing our client-side code.

Since we now send over a room object instead of just a name we have to adjust a few existing methods:

// public/assets/app.js
handleChatMessage(msg) {
  const room = this.findRoom(msg.target.id);
  if (typeof room !== "undefined") {
    room.messages.push(msg);
  }
},

sendMessage(room) {
    if (room.newMessage !== "") {
        this.ws.send(JSON.stringify({
        action: 'send-message',
        message: room.newMessage,
        target: {
            id: room.id,
            name: room.name
        }
        }));
        room.newMessage = "";
    }
},

findRoom(roomId) {
    for (let i = 0; i < this.rooms.length; i++) {
        if (this.rooms[i].id === roomId) {
        return this.rooms[i];
        }
    }
},
Enter fullscreen mode Exit fullscreen mode

And at last, add the methods for joining a private room and responding to room-joined action from the server:

// public/assets/app.js

handleNewMessage(event) {
  ....
  case "room-joined":
    this.handleRoomJoined(msg);
    break;
}

handleRoomJoined(msg) {
  room = msg.target;
  room.name = room.private ? msg.sender.name : room.name;
  room["messages"] = [];
  this.rooms.push(room);
},

// we removed a line in the function below
// since we only save new rooms when the server tells so
joinRoom() {
  this.ws.send(JSON.stringify({ action: 'join-room', message: this.roomInput }));
  this.roomInput = "";
},

joinPrivateRoom(room) {
  this.ws.send(JSON.stringify({ action: 'join-room-private', message: room.id }));
}
Enter fullscreen mode Exit fullscreen mode

Ok that’s it! Let’s see our chat server application in action!

What’s next?

In the next part will leverage Redis Pub/Sub for scalability (or for fun?), so stay tuned!

Feel free to leave a comment when you have suggestions or questions!

The final source code of this part van be found here:

https://github.com/jeroendk/go-vuejs-chat/tree/v2.0

The post Multi-room Chat Application With WebSockets In Go And Vue.js (Part 2) appeared first on Which Dev.

Top comments (1)

Collapse
 
nimzo profile image
nimzo

Excellent tutorial. Just what I was looking for.
Just a slight issue: To avoid CORS, I needed to change localhost in app.js

serverUrl: "ws://127.0.0.1:8080/ws",