Risu | Chat module

Dev time: 7 days
Project: https://github.com/datyayu/risu.moe
Latest commit: e5af3d9

So we reach day 7 and the after 3 days working in the chat module, it has reached a point where it’s pretty much ready.

Here’s how the current version looks.

ss.png

This update was only related to the chat module. The online users lists and messages are now updated on real-time from firebase. User names are now color-customizable and the user info is now stored locally, so it persists across sessions.

The !skip and !reset commands are now enabled as a setup to the upcoming playlist module. Also, the system (aka a firebase cloud function) automatically publishes a message whenever a user uses a command.


On this stage of the chat development, there were 4 main elements that I had to focus on in order to get it working properly.

  • Elm ports’ usage.
  • Firebase’s real-time database as a presence system.
  • Firebase’s real-time database to store messages.
  • Cloud functions to parse commands.

So let’s go over them once at a time.


Elm ports

First are the elm ports. Due to firebase not having an elm library to interface with its services, we have to use the javascript one via elm ports.

For those who don’t know elm,  in order to keep the purity and guaranties that the language provides we need to use ports to communicate between elm and javascript.

From elm, they look kinda like this:

-- Output port
port sendMessage : ExternalMessage -> Cmd msg

-- Input port
port newMessage : (ExternalMessage -> msg) -> Sub msg

The output port takes an ExternalMessage type record which is sent to javascript whenever we evoke that function.

The input port receives an ExternalMessage and returns a subscription which gets called whenever we pass a new value to the port via javascript.

On the javascript side, we handle ports like this:

app.ports.sendMessage.subscribe(function(message) {
    // handle message
});

app.ports.newMessage.send(message);

Basically we subscribe to the elm output port and we send data using the input port.

Anyway, the chat module uses 5 ports:

  • setUser (output). When the user sets the nickname or the color we notify javascript so it can be store in localStorage and also publish the update to firebase.
  • updateUser (input). This one is called  when the user is retrieved from local storage and it updates elm’s internal user with that info.
  • setUsers (input). Updates the online user list when it changes.
  • sendMessage (output). Outputs the message we want to post to firebase.
  • newMessage (input). Appends a new message to the chat when it’s added to firebase.

And that’s it. Basically they act as a bridge between elm and our firebase database and localStorage.


Firebase as a presence system.

Since we want to know who is connected at any time, we need to keep track of every client using the app.

There isn’t much stuff going on here. Just something like this:

// Get local user id or create a new one
var userId = 
    localStorage.getItem('firebase-user-id') 
    || db.ref('presence').push().key;

var userRef = db.ref('presence/' + userId);

// Remove user from db on disconnect. 
userRef.onDisconnect().remove();

// Add local user to db.
userRef.set(localUser);

First we get the user ID, which we can pull from localStorage or we generate a new one by adding an entry to the db. After that we create a reference to that entry (userRef) and we tell firebase to remove that entry when we disconnect, so only the online users will be stored on the database. Finally we just push the local user’s info to that entry so it can be retrieved by other users.

And that’s it, that’s how that part works.


Storing messages on firebase.

This one is pretty straightforward.

var messages = db.ref('messages')
    .orderByChild('time')
    .limitToLast(10);

messages.on('child_added', function(snapshot) { 
    var message = snapshot.val(); 
    if (!message) return; 

    app.ports.newMessage.send(message); 
})

We just subscribe to get notified when a message is added and then send that new message to elm.

var messageRef = db.ref('messages').push(); 

messageRef.set(message);

To post a message we simply create a new record on the database and then we fill that record with the message info.


Cloud functions

This was actually a new concept to me.

While I was working on the real-time database related tasks, I came across cloud functions on the firebase documentation, so I gave it a try and it just worked.

Cloud functions are basically functions that only run on certain events. Unlike a whole server that is constantly available and working, a cloud function is just a piece a code that firebase only runs whenever a certain event happens.

Here’s an example:

const functions = require('firebase-functions'); 
const admin = require('firebase-admin'); 

admin.initializeApp(functions.config().firebase);

exports.onNewMessage = functions.database.ref('/messages/{msgId}') 
    .onWrite(event => {
        // Do something
        console.log('Hello from cloud')
    });

This is all the code you need to deploy a cloud function. In this case , we listen for the onWrite event of the /messages/{msgId} path. This means than whenever a new message is written our function will be executed.

This approach has the advantage that you don’t need a full server, easier scalability and pricing will be based on the usage.

In this app, I used a cloud function to handle two things:

  • Whenever a new message gets added, update the time property to the firebase local time. This is so we don’t have to depend on the user’s computer time, as this can be wrong.
  • Whenever a new message gets added, if it’s a command (!skip or !reset) then update the skip vote count in the database, and then publish a message to the chat announcing that update. This doesn’t have yet a real use case but it’s a setup for when we get into the playlist module.

The usage of firebase’s cloud functions was pretty simple and most of the stuff I needed to get it working was on the documentation. The only thing I had to research is how to get the server’s time on firebase, which it turns out can be obtained using:

const admin = require('firebase-admin');

var serverTime = admin.database.ServerValue.TIMESTAMP

…And that’s most of the stuff I had to deal with while working on the chat module. Overall was actually a pretty smooth experience working with firebase. There were moments where I couldn’t get stuff working but once you get how to work with the sdk it’s super simple to use.

Now that I’m done with this module, I think the next part to work on will be the drag and drop, where you drop and audio file into the window and it gets uploaded. While I was working on the chat I came across firebase’s cloud storage so I think that is what I’m going to use to store the files.

Once I’m finish with that, I’ll write about it. Until then, cya!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s