In this blog post, we will build a simple social network with realtime features and a list of all members who are online.
We will be using Node.js as the application server, Vanilla JavaScript in the front end and Pusher for realtime communication between our server and front end.
We will build an app which will be like your friends list or a common chat room where you can see who's online and their latest status update in realtime. In the blog post, we will learn about Pusher's presence channel and how to know about the online members to this channel.
We will be building the following components during this blog post:
/register
API - In order to register/login a new user to our channel and server by creating their session and saving their info/isLoggedIn
API - To check if a user is already logged in or not in case of refreshing the browser/usersystem/auth
API - Auth validation done by Pusher after registering it with our app and on subscribing to a presence or private channel/logout
API - To logout the user and remove the sessionYou can create a free account in Pusher here. After you signup and login for the first time, you will be asked to create a new app as seen in the picture below. You will have to fill in some information about your project and also the front end library or backend language you will be building your app with.
For this particular blog post, we will be selecting Vanilla JavaScript for the frontend and Node.js for the backend as seen in the picture above. This will just show you a set of starter sample codes for these selections, but you can use any integration kit later on with this app.
Node.js should be installed in the system as a prerequisite to this. Now let us begin building the Node.js server and all the required APIs using Express. Initialise a new node project by the following command
npm init
We will be installing the required dependencies like Express, express-session, Pusher, body-parser, cookie-parser by the following command:
npm install express express-session body-parser cookie-parser --save
We will now create the basic foundation for Node Server and also enable sessions in that using express-session module.
1var express = require('express'); 2var path = require('path'); 3var bodyParser = require('body-parser'); 4var expressSession = require('express-session'); 5var cookieParser = require('cookie-parser'); 6 7var app = express(); 8 9// must use cookieParser before expressSession 10app.use(cookieParser()); 11 12app.use(expressSession({ 13 secret:'<some-secret-token-here>', 14 resave: true, 15 saveUninitialized: true 16})); 17 18app.use(bodyParser.json()); 19app.use(bodyParser.urlencoded({ extended: false })); 20app.use(express.static(path.join(__dirname, 'public'))); 21 22// Error Handler for 404 Pages 23app.use(function(req, res, next) { 24 var error404 = new Error('Route Not Found'); 25 error404.status = 404; 26 next(error404); 27}); 28 29module.exports = app; 30 31app.listen(9000, function(){ 32 console.log('Example app listening on port 9000!') 33});
In the above code, we have created a basic Express server and using the method .use
we have enabled cookie-parser, body-parser and a static file serving from public
folder. We have also enabled sessions using express-session
module. This will enable us to save user information in the appropriate request session for the user.
Pusher has an open source NPM module for Node.js integrations which we will be using. It provides a set of utility methods to integrate with Pusher APIs using a unique appId, key and a secret. We will first install the Pusher npm
module using the following command:
npm install pusher --save
Now, we can use 'require' to get the Pusher module and to create a new instance passing an options object with important keys to initialise our integration. For this blog post, I have put random keys; you will have to obtain it for your app from the Pusher dashboard.
1var Pusher = require('pusher'); 2 3var pusher = new Pusher({ 4 appId: '30XXX64', 5 key: '82XXXXXXXXXXXXXXXXXb5', 6 secret: '7bXXXXXXXXXXXXXXXX9e', 7 encrypted: true 8}); 9 10var app = express(); 11...
You will have to replace the appId
, key
and a secret
with values specific to your own app. After this, we will write code for a new API which will be used to create a new comment.
Now, we will develop the first API route of our application through which a new user can register/login itself and make itself available on our app.
1app.post('/register', function(req, res){ 2 console.log(req.body); 3 if(req.body.username && req.body.status){ 4 var newMember = { 5 username: req.body.username, 6 status: req.body.status 7 } 8 req.session.user = newMember; 9 res.json({ 10 success: true, 11 error: false 12 }); 13 }else{ 14 res.json({ 15 success: false, 16 error: true, 17 message: 'Incomplete information: username and status are required' 18 }); 19 } 20});
In the above code, we have exposed a POST API call on the route /register
which would expect username and status parameters to be passed in the request body. We will be saving this user info in the request session.
In order to enable any client subscribing to Pusher Private and Presence channels, we need to implement an auth API which would authenticate the user request by calling Pusher.authenticate method at the server side. Add the following code in the server in order to fulfil this condition:
1app.post('/usersystem/auth', function(req, res) { 2 var socketId = req.body.socket_id; 3 var channel = req.body.channel_name; 4 var currentMember = req.session.user; 5 var presenceData = { 6 user_id: currentMember.username, 7 user_info: { 8 status: currentMember.status, 9 } 10 }; 11 var auth = pusher.authenticate(socketId, channel, presenceData); 12 res.send(auth); 13});
We need to provide the specific route in the initialisation of Pusher Client side library which we will see later in this blog post. The Pusher client library will automatically call this route and pass in the channel_name and socket_id properties. We will simultaneously get the user information from the user session object and pass it as presenceData to the Pusher.authenticate method call.
If the user refreshes the browser, the client side app should detect if the user is already registered or not. We will implement an isLoggedIn API route for this. Also, we need a logout route to enable any user to logout from the app.
1app.get('/isLoggedIn', function(req,res){ 2 if(req.session.user){ 3 res.send({ 4 authenticated: true 5 }); 6 }else{ 7 res.send({ authenticated: false }); 8 } 9}); 10 11app.get('/logout', function(req,res){ 12 if(req.session.user){ 13 req.session.user = null; 14 } 15 res.redirect('/'); 16});
We will be developing the front end app now to register a new user with an initial status, see the members who are online and their statuses. We will also build the feature for the logged in user to update their users and all other users will see the updated status in realtime.
We have already written code in our server.js
to serve static content from public
folder, so we will write all our front end code in this folder.
Please create a new folder public
and also create an empty index.html
for now.
We will be adding some basic boilerplate code to set up the base structure for our web app like Header, Sections where registration form and the members list can be placed.
1<!DOCTYPE> 2<html> 3 <head> 4 <title>Whats Up ! Know what other's are up to in Realtime !</title> 5 <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.2/build/pure-min.css" integrity="sha384-UQiGfs9ICog+LwheBSRCt1o5cbyKIHbwjWscjemyBMT9YCUMZffs6UqUTd0hObXD" crossorigin="anonymous"> 6 <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:200"> 7 <link rel="stylesheet" href="./style.css"> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9 </head> 10 <body> 11 <header> 12 <div class="logo"> 13 <img src="./assets/pusher-logo.png" /> 14 </div> 15 <div id="logout" class="logout"> 16 <a href="/logout">Logout</a> 17 </div> 18 </header> 19 <section class="subheader"> 20 <img class="whatsup-logo" src="./assets/whatsup.png" /> 21 <h2>Whats Up ! Know what other's are up to in Realtime !</h2> 22 </section> 23 <section> 24 <div id="loader" class="loader"> 25 </div> 26 <script id="member-template" type="text/x-template"> 27 </script> 28 <div id="me" class="me"> 29 </div> 30 <div id="membersList" class="members-list"> 31 </div> 32 <div id="signup-form" class="tab-content"> 33 <div class="header"> 34 <div><img src="./assets/comments.png"></div> 35 <div class="text">First Time Sign Up !</div> 36 </div> 37 <form class="pure-form" id="user-form"> 38 <div class="signup-form"> 39 <div class="left-side"> 40 <div class="row"> 41 <input type="text" required placeholder="enter a username or displayname" id="display_name"> 42 </div> 43 <div class="row"> 44 <textarea placeholder="enter initial status text" required id="initial_status" rows="3"></textarea> 45 </div> 46 </div> 47 <div class="right-side"> 48 <button 49 type="submit" 50 class="button-secondary pure-button">Signup/Login</button> 51 </div> 52 </div> 53 </form> 54 </div> 55 </section> 56 <script src="https://js.pusher.com/4.0/pusher.min.js"></script> 57 <script type="text/javascript" src="./app.js"></script> 58 </body> 59</html>
In the above boilerplate code, we have referenced our main Javascript file app.js
and the Pusher client side JS library. We also have a script tag where we will place the template for a member row in the member list. Also, we have two empty div tags with ids me and membersList to contain the logged in member name and info, as well as the list of all other members with their statuses.
Important to note that we will be showing the signup form for the first time and the MembersList and Logout button will be hidden by default initially. Please create a new file called style.css
and add the following css to it:
1body{ 2 margin:0; 3 padding:0; 4 overflow: hidden; 5 font-family: Raleway; 6} 7 8header{ 9 background: #2b303b; 10 height: 50px; 11 width:100%; 12 display: flex; 13 color:#fff; 14} 15 16 17.loader, 18.loader:after { 19 border-radius: 50%; 20 width: 10em; 21 height: 10em; 22} 23.loader { 24 margin: 60px auto; 25 font-size: 10px; 26 position: relative; 27 text-indent: -9999em; 28 border-top: 1.1em solid rgba(82,0,115, 0.2); 29 border-right: 1.1em solid rgba(82,0,115, 0.2); 30 border-bottom: 1.1em solid rgba(82,0,115, 0.2); 31 border-left: 1.1em solid #520073; 32 -webkit-transform: translateZ(0); 33 -ms-transform: translateZ(0); 34 transform: translateZ(0); 35 -webkit-animation: load8 1.1s infinite linear; 36 animation: load8 1.1s infinite linear; 37} 38@-webkit-keyframes load8 { 39 0% { 40 -webkit-transform: rotate(0deg); 41 transform: rotate(0deg); 42 } 43 100% { 44 -webkit-transform: rotate(360deg); 45 transform: rotate(360deg); 46 } 47} 48@keyframes load8 { 49 0% { 50 -webkit-transform: rotate(0deg); 51 transform: rotate(0deg); 52 } 53 100% { 54 -webkit-transform: rotate(360deg); 55 transform: rotate(360deg); 56 } 57} 58 59 60.subheader{ 61 display: flex; 62 align-items: center; 63 margin: 0px; 64} 65 66.whatsup-logo{ 67 height:60px; 68 border-radius: 8px; 69 flex:0 60px; 70 margin-right: 15px; 71} 72 73.logout{ 74 flex:1; 75 justify-content: flex-end; 76 padding:15px; 77 display: none; 78} 79 80.logout a{ 81 color:#fff; 82 text-decoration: none; 83} 84 85#signup-form{ 86 display: none; 87} 88 89input, textarea{ 90 width:100%; 91} 92 93 94section{ 95 padding: 0px 15px; 96} 97 98.logo img{ 99 height: 35px; 100 padding: 6px; 101 margin-left: 20px; 102} 103 104#updateStatus{ 105 display: none; 106} 107 108.members-list{ 109 display: none; 110 flex-direction: column; 111} 112 113.me { 114 display: none; 115}
Please try to open the URL http://localhost:9000
in your browser and the application will load with the basic register or login form with username and status. The output will look like the screenshot below:
Now we will add our Javascript code to have basic utility elements inside a self invoking function to create a private scope for our app variables. We do not want to pollute JS global scope.
1// Using IIFE for Implementing Module Pattern to keep the Local Space for the JS Variables 2(function() { 3 // Enable pusher logging - don't include this in production 4 Pusher.logToConsole = true; 5 6 var serverUrl = "/", 7 members = [], 8 pusher = new Pusher('73xxxxxxxxxxxxxxxdb', { 9 authEndpoint: '/usersystem/auth', 10 encrypted: true 11 }), 12 channel, 13 userForm = document.getElementById("user-form"), 14 memberTemplateStr = document.getElementById('member-template').innerHTML; 15 16 function showEle(elementId){ 17 document.getElementById(elementId).style.display = 'flex'; 18 } 19 20 function hideEle(elementId){ 21 document.getElementById(elementId).style.display = 'none'; 22 } 23 24 function ajax(url, method, payload, successCallback){ 25 var xhr = new XMLHttpRequest(); 26 xhr.open(method, url, true); 27 xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); 28 xhr.onreadystatechange = function () { 29 if (xhr.readyState != 4 || xhr.status != 200) return; 30 successCallback(xhr.responseText); 31 }; 32 xhr.send(JSON.stringify(payload)); 33 } 34 35 ajax(serverUrl+"isLoggedIn","GET",{},isLoginChecked); 36 37 function isLoginChecked(response){ 38 var responseObj = JSON.parse(response); 39 if(responseObj.authenticated){ 40 channel = pusher.subscribe('presence-whatsup-members'); 41 bindChannelEvents(channel); 42 } 43 updateUserViewState(responseObj.authenticated); 44 } 45 46 function updateUserViewState(isLoggedIn){ 47 document.getElementById("loader").style.display = "none"; 48 if(isLoggedIn){ 49 document.getElementById("logout").style.display = "flex"; 50 document.getElementById("signup-form").style.display = "none"; 51 }else{ 52 document.getElementById("logout").style.display = "none"; 53 document.getElementById("signup-form").style.display = "block"; 54 } 55 } 56 57 function showLoader(){ 58 document.getElementById("loader").style.display = "block"; 59 document.getElementById("logout").style.display = "none"; 60 document.getElementById("signup-form").style.display = "none"; 61 } 62 63 // Adding a new Member Form Submit Event 64 userForm.addEventListener("submit", addNewMember); 65 66 67 function addNewMember(event){ 68 event.preventDefault(); 69 var newMember = { 70 "username": document.getElementById('display_name').value, 71 "status": document.getElementById('initial_status').value 72 } 73 showLoader(); 74 ajax(serverUrl+"register","POST",newMember, onMemberAddSuccess); 75 } 76 77 function onMemberAddSuccess(response){ 78 // On Success of registering a new member 79 console.log("Success: " + response); 80 userForm.reset(); 81 updateUserViewState(true); 82 // Subscribing to the 'presence-members' Channel 83 channel = pusher.subscribe('presence-whatsup-members'); 84 bindChannelEvents(channel); 85 } 86})();
In the above code, we have referenced all the important variables we will be requiring. We will also initialise the Pusher library using new Pusher and passing the api key as the first argument. The second argument contains an optional config object in which we will add the key authEndpoint
with the custom node api route /usersystem/auth
and also add the key encrypted
setting it to value true.
We will create a couple of generic functions to show or hide an element passing its unique id. We have also added a common method named ajax to make ajax requests using XMLHttp object in Vanilla JavaScript.
At the load of the page we make an ajax request to check if the user is logged in or not. If the user is logged in, we will directly use the Pusher instance to subscribe the user to a presence channel named presence-whatsup-members
, you can have this as the unique chat room or app location where you want to report/track the online members.
We have also written a method above to addNewMember
using an ajax request to the register
api route we have built in Node.js. We will be passing the name and initial status entered into the form.
We also have a method to update the user view state based on the logged in status. This method does nothing but updates the visibility of members list, logout button and signup form. We have used a bindChannelEvents
method when the user is logged in which we will be implementing later in the blog post.
Please add the following css in style.css
file to display the me
element appropriately with the username and the status of the logged in user.
1.me { 2 border:1px solid #aeaeae; 3 padding:10px; 4 margin:10px; 5 border-radius: 10px; 6} 7 8.me img{ 9 height: 40px; 10 width: 40px; 11} 12 13.me .status{ 14 padding:5px; 15 flex:1; 16} 17 18.me .status .username{ 19 font-size:13px; 20 color: #aeaeae; 21 margin-bottom:5px; 22} 23 24.me .status .text{ 25 font-size: 15px; 26 width:100%; 27 -webkit-transition: all 1s ease-in 5ms; 28 -moz-transition: all 1s ease-in 5ms; 29 transition: all 1s ease-in 5ms; 30}
Now, after subscribing to the channel, we need to bind certain events so that we can know whenever a new member is added to the channel or removed from it. We will also bind to a custom event to know whenever someone updates their status.
Add the following code to the app.js
file:
1// Binding to Pusher Events on our 'presence-whatsup-members' Channel 2 3 function bindChannelEvents(channel){ 4 channel.bind('client-status-update',statusUpdated); 5 var reRenderMembers = function(member){ 6 renderMembers(channel.members); 7 } 8 channel.bind('pusher:subscription_succeeded', reRenderMembers); 9 channel.bind('pusher:member_added', reRenderMembers); 10 channel.bind('pusher:member_removed', reRenderMembers); 11 }
In the above bindChannelEvents
method, we use the channel.bind
method to bind event handlers for 3 internal events - pusher:subscription_succeeded
, pusher:member_added
, pusher:member_removed
and 1 custom event - client-status-update
.
Now we will add the JavaScript code to render the list of members. It is important to know that the object which i returned from the .subscribe
method has a property called members
which can be used to know the information about the logged in user referred by the key me
and other members by key members
. Add the following code to app.js
file
1// Render the list of members with updated data and also render the logged in user component 2 3 function renderMembers(channelMembers){ 4 var members = channelMembers.members; 5 var membersListNode = document.createElement('div'); 6 showEle('membersList'); 7 8 Object.keys(members).map(function(currentMember){ 9 if(currentMember !== channelMembers.me.id){ 10 var currentMemberHtml = memberTemplateStr; 11 currentMemberHtml = currentMemberHtml.replace('{{username}}',currentMember); 12 currentMemberHtml = currentMemberHtml.replace('{{status}}',members[currentMember].status); 13 currentMemberHtml = currentMemberHtml.replace('{{time}}',''); 14 var newMemberNode = document.createElement('div'); 15 newMemberNode.classList.add('member'); 16 newMemberNode.setAttribute("id","user-"+currentMember); 17 newMemberNode.innerHTML = currentMemberHtml; 18 membersListNode.appendChild(newMemberNode); 19 } 20 }); 21 renderMe(channelMembers.me); 22 document.getElementById("membersList").innerHTML = membersListNode.innerHTML; 23 } 24 25 26 function renderMe(myObj){ 27 document.getElementById('myusername').innerHTML = myObj.id; 28 document.getElementById('mystatus').innerHTML = myObj.info.status; 29 }
We have added the event handler for new member add/remove event to re-render the members list so that it remains updated with the online members only. In order to show the members list we need to add the following style into our file style.css
.
1.member{ 2 display: flex; 3 border-bottom: 1px solid #aeaeae; 4 margin-bottom: 10px; 5 padding: 10px; 6} 7 8.member .user-icon{ 9 flex:0 40px; 10 display: flex; 11 align-items: center; 12 justify-content: center; 13} 14 15.member .user-icon img{ 16 width:50px; 17 height:50px; 18} 19 20.member .user-info{ 21 padding:5px; 22 margin-left:10px; 23} 24 25.member .user-info .name{ 26 font-weight: bold; 27 font-size: 16px; 28 padding-bottom:5px; 29} 30 31.member .user-info .status{ 32 font-weight: normal; 33 font-size:13px; 34} 35 36.member .user-info .time{ 37 font-weight: normal; 38 font-size:10px; 39 color:#aeaeae; 40}
Now we will write the code, to trigger a client event on our channel to notify all users about the status change of the logged in user. Add the following code to your app.js
file.
1// On Blur of editting my status update the status by sending Pusher event 2 document.getElementById('mystatus').addEventListener('blur',sendStatusUpdateReq); 3 4 function sendStatusUpdateReq(event){ 5 var newStatus = document.getElementById('mystatus').innerHTML; 6 var username = document.getElementById('myusername').innerText; 7 channel.trigger("client-status-update", { 8 username: username, 9 status: newStatus 10 }); 11 } 12 13 // New Update Event Handler 14 // We will take the Comment Template, replace placeholders and append to commentsList 15 function statusUpdated(data){ 16 var updatedMemberHtml = memberTemplateStr; 17 updatedMemberHtml = updatedMemberHtml.replace('{{username}}',data.username); 18 updatedMemberHtml = updatedMemberHtml.replace('{{status}}',data.status); 19 updatedMemberHtml = updatedMemberHtml.replace('{{time}}','just now'); 20 document.getElementById("user-"+data.username).style.color = '#1B8D98'; 21 document.getElementById("user-"+data.username).innerHTML=updatedMemberHtml; 22 setTimeout(function(){ 23 document.getElementById("user-"+data.username).style.color = '#000'; 24 },500); 25 }
IMPORTANT: When we run this code in our browsers, update the status and blur out of the status control, we will get an error in the JavaScript console for the Pusher library. To fix this, go to the console at Pusher.com website, go to settings and enable sending events from clients directly.
We can only send events from client sdirectly for Presence or Private channels - more information here.
1Pusher : Error : { 2 "type":"WebSocketError", 3 "error":{ 4 "type":"PusherError", 5 "data": 6 { 7 "code":null, 8 "message":"To send client events, you must enable this feature in the Settings page of your dashboard." 9 } 10 } 11}
We have built an application which will display all the online members for a particular presence channel and their updates. If any of the online user updates their status, every user will be notified about the updated status.
This component or code can be used for developing a social networking section in most of the web apps these days. It is an important use case where the user needs to know about other available participants. For example: an online classroom app can see the other participants and the status can correspond to any question any participant wants to ask the presenter.
We have just used Node.js and Vanilla JavaScript to implement the above functionality. You can use the JavaScript for frontend code with any popular framework like React or Angular.js and for the backend can also be Java or Ruby. Please refer to the Pusher docs for more information on this.