Build a chat app in Flask and Vue with sentiment analysis - Part 4: Adding an online presence feature

Introduction

In this part, we’ll see how to add a “who’s online” feature. This feature will make our chat more lively. This way we know if the other participants are online so we know if we’ll be getting a reply soon. The code is available on GitHub.

You can find the previous parts of the series here: Part 1 Part 2 Part 3

If you don't have the setup from previous parts, follow the below instructions to get it otherwise skip this.

1# Clone the repo
2    $ git clone https://github.com/dongido001/pusher-chat-sentiment-analysis.git -b part-3
3    
4    # Go to the project root folder
5    $ cd pusher-chat-sentiment-analysis
6    
7    # Install dependencies
8    $ npm install

Update the .env file in the project’s root folder with your correct Pusher App key:

1VUE_APP_PUSHER_KEY=<PUSHER_APP_KEY>
2    VUE_APP_PUSHER_CLUSTER=<PUSHER_APP_CLUSTER>

Then run the app:

1# Start vue app
2    $ npm run serve

Then from a new terminal execute the following:

1$ cd api
2    $ python -m venv env
3    $ pip install -r requirements.txt
4    $ source env/bin/activate

Update the api/.env file with your correct Pusher API key:

1PUSHER_APP_ID=app_id
2    PUSHER_KEY=key
3    PUSHER_SECRET=secret
4    PUSHER_CLUSTER=cluster
    $ flask run

Now you should have both apps running. Both apps should be running on a different port. Note the URL of the Vue.js app in your terminal because this is what you will use to access the chat application.

Presence channels

Presence channels are similar to the private chat channel. It exposes an additional feature of an awareness of who is subscribed to a channel. This will enable us to easily implement “who’s online” functionality into our chat app. When naming a presence channel, it needs to have a prefix of “presence-”.

The flow is much related to the private channel. First, we will subscribe all users logging in to the app to a presence channel we’ll name “presence-chitchat”. Before they are subscribed, Pusher will make a request to our auth endpoint (which we have already) to authenticate the channel.

An important thing to note is that when setting up the authentication for presence channel, you must add a custom data to the authenticate function. The custom data is the information of the user you want to authenticate. The custom data will be passed back to your client’s app so we can use them.

Authenticating channel

First, let’s update the endpoint so that it accommodates authentication for presence channels.

When Pusher makes a request to this endpoint to authenticate the channel, it passes along the channel name and the connected user’s socket ID.

Remember that our routes are protected with JWT. We are using the @jwt_required decorator to protect the route. When Channels makes the request to the endpoint, it includes a JWT token in its request header. If the token is not valid, it results to a 403 HTTP error. Also, remember that we set the JWT token to be included in the request header while we are initializing Pusher JavaScript client.

Update the pusher_authentication function in api/app.py to include custom data:

1# ./api/app.py
2    
3    [...]
4    @app.route("/api/pusher/auth", methods=['POST'])
5    @jwt_required
6    def pusher_authentication():
7        channel_name = request.form.get('channel_name')
8        socket_id = request.form.get('socket_id')
9        
10        username = get_jwt_identity()
11        
12        user_data = User.query.filter_by(username=username).first()
13        
14        auth = pusher.authenticate(
15            channel=channel_name,
16            socket_id=socket_id,
17            custom_data={
18                "user_id": user_data.id,
19                "user_info": {
20                   "username": user_data.username
21                }
22            }
23        )
24        
25        return jsonify(auth)
26    [...]

In the code above,

  • We first fetch the channel name and socket ID from the request.
  • Then, we fetch the username from the JWT token.
  • Next, we query the database with the username to get more information about the user.
  • Finally, we call pusher.authenticate to authenticate the channel. We also added some custom data to the authenticate function. The user_id in the custom_data is the ID of the user while the user_info property is for additional information for that user.

Now, with this, we can authenticate both our private and presence channel.

IMPORTANT: If you don’t include a custom data, the channel won’t be authenticated. This means you won’t be able to subscribe to the channel. The user_id in the custom data is also required.

Subscribe the user

Now, let’s subscribe our users to a common channel from the Vue app. We’ll name this channel presence-chatchit.

Add the following code to the setAuthenticated method in src/App.vue:

    var presenceChannel = pusher.subscribe("presence-chitchat");

Once the user logs in, we’ll subscribe the user to the presence-chitchat channel.

Next, update the user is_online status to true as they subscribe. Add the below code to the setAuthenticated method in src/App.vue:

1[...]
2    presenceChannel.bind("pusher:member_added", data => {
3      // Get the index of user that just scubscribed
4      const index = this.users.findIndex(user => user.id == data.id);
5      
6      // Set the is_online status of the user to true
7      this.$set(this.users, index, { ...this.users[index], is_online: true });
8    });
9    [...]

Here,

  • We bind the channel to the pre-defined event called pusher:member_added. This event is available by default for the presence channels. In the event, we can access the custom data we added while authenticating the channel.
  • this.users is a state we used for storing all users available on the app.
  • the data is the custom data we passed while authenticating the channel.
  • Then finally, we set the is_online status for the user to be true.

Next, update the user is_online status to false as they leave the channel. Add the below code to the setAuthenticated method in src/App.vue:

1[...]
2    presenceChannel.bind("pusher:member_removed", data => {
3      // Get the index of user that just subscribed
4      const index = this.users.findIndex(user => user.id == data.id);
5      
6      // Set the is_online status of the user to false
7      this.$set(this.users, index, {
8        ...this.users[index],
9        is_online: false
10      });
11    });
12    [...]

Here, we bind the channel to the pre-defined event called pusher:member_removed. This event is available by default for the presence channels. Then finally, we set the is_online status for the user to be false.

Next, get all users already on the channel before the user joined and set their is_online status to be true. Add the below code to the setAuthenticated method in src/App.vue:

1[...]
2    presenceChannel.bind("pusher:subscription_succeeded", data => {
3      // Fetch members already on this channel, then set them to be online
4      for (let member_id of Object.keys(data.members)) {
5        const index = this.users.findIndex(user => user.id == member_id);
6        this.$set(this.users, index, {
7          ...this.users[index],
8          is_online: true
9        });
10      }
11    });
12    [...]

The pusher:subscription_succeeded is triggered as soon as a user subscribes to the channel. As the user subscribes to the channel, it’s possible to have users already subscribed to that channel.

In the code above, when we get the event, we’ll fetch all the users already subscribed to the channel and then update their is_online property to true.

Adding an online indicator

We need an indicator that shows if a user is online or offline. We’ll use a small circle beside a user to show when the user is online, and remove it when the user is offline.

In the template section of src/components/Users.vue, add the below markup:

1<template>
2      <div style="margin-top: 0px;">
3        <div v-for="(user, id) in users" v-bind:key="id">
4          <div
5            v-bind:class="[activeUser == user.id ? 'user active' : 'user']"
6            @click="chat(user.id)"
7          >
8            {{user.userName}}
9            <span v-if="user.has_new_message" class="has_new_message">New message</span>
10            <span v-if="user.is_online" class="online"></span>
11          </div>
12        </div>
13      </div>
14    </template>

Here, we check if the user is online so that we can show the HTML indicator.

Next, add some style for the indicator in the <style> section of the src/components/Users.vue file:

1[...]
2    .online {
3      height: 15px;
4      width: 15px;
5      background-color: #17a2b8;
6      border-radius: 50%;
7      display: inline-block;
8      margin-bottom: -4px;
9      border: 1px solid white;
10    }
11    [...]

Testing the app

Congrats! Now we can see those online and those that are offline.

Now test the app. Open the app in different tabs on your browser then log in. You will get a similar experience as below!

flask-vue-sentiment-demo-part-4

Conclusion

In this tutorial, we explored how to add a “who’s online” feature to chat apps using Channel’s presence channel.

The source code for the tutorial is available in GitHub.