Build a realtime Instagram clone — Part 3: Realtime feed updates with Pusher and desktop notifications

  • Christian Nwamba
April 27th, 2018
You should have completed the previous parts of the series.

This is part 3 of a 4 part tutorial. You can find part 1 here, part 2 here and part 4 here.

In the last part of this series, we looked at how to connect the GraphQL server to our React Instagram clone allowing for dynamic posts to be viewed on the homepage. Now, to give users a seamless and fluid experience when interacting with the application, let’s add realtime functionality to it. This will update feeds as new posts are created and a notification system will also be put in place to allow for this.

To make this possible, Pusher is going to be integrated into the application to make it easier to bring realtime functionality without worrying about infrastructure.

Prerequisites

  • Should have read previous parts of the series
  • Basic knowledge of JavaScript
  • Node installed on your machine
  • NPM installed on your machine

Configure Pusher on the server

To get started with Pusher, create a developer account. Once you do this, create your application and get your application keys.

Note your application keys as you will need them later on in the article

Install the Node modules

Once you do that, you will need to install the Node modules needed for the application to work in the server directory of the application:

    npm install pusher connect-multiparty body-parser --save
  • pusher to integrate realtime functionality
  • body-parser and connect-multiparty to handle incoming requests

Import the Node modules

Now that the necessary modules have been installed, the next thing is to import them for use in the server/server.js file. Edit it to look like this:

    // server/server.js
    [...]

    let Pusher = require("pusher");
    let bodyParser = require("body-parser");
    let Multipart = require("connect-multiparty");

    [...]

Configure the Pusher client

You will also need to configure your Pusher client to allow you to trigger events. To do this, add the following to the server.js file:

    // server/server.js
    [...]

    let pusher = new Pusher({
      appId: 'PUSHER_APP_ID',
      key: 'PUSHER_APP_KEY',
      secret: 'PUSHER_APP_SECRET',
      cluster: 'PUSHER_CLUSTER',
      encrypted: true
    });

    // create express app
    [...]

Creating the endpoint for storing new posts

To simulate the effect of creating a new post, a new endpoint is added to the application as follows:

    // server/server.js

    // add Middleware
    let multipartMiddleware = new Multipart();

    // trigger add a new post 
    app.post('/newpost', multipartMiddleware, (req,res) => {
      // create a sample post
      let post = {
        user : {
          nickname : req.body.name,
          avatar : req.body.avatar
        },
        image : req.body.image,
        caption : req.body.caption
      }

      // trigger pusher event 
      pusher.trigger("posts-channel", "new-post", { 
        post 
      });

      return res.json({status : "Post created"});
    });

    // set application port
    [...]

When a post request is made to the /post route, the data submitted is then used to construct a new post and then the new-post event is triggered in the post-channel and a response is sent to the client making the request.

Configure Pusher on the client

Now that the server has been configured, the next thing that needs to be done is to get Pusher working in our React application. To do this, let’s install the JavaScript Pusher module in the root of the instagram-clone directory:

    npm install pusher-js

Set up the Pusher client

Now that the module is installed, the Pusher module needs to be used. Edit the src/App.js like this:

    // src/App.js

    import React, {Component} from 'react';
    [...]
    // import pusher module
    import Pusher from 'pusher-js';

    // set up graphql client
    [...]

    // create component
    class App extends Component{
      constructor(){
        super();
        // connect to pusher
        this.pusher = new Pusher("PUSHER_APP_KEY", {
         cluster: 'eu',
         encrypted: true
        });
      }

      render(){
        return (
          <ApolloProvider client={client}>
            <div className="App">
              <Header />
              <section className="App-main">
                {/* pass the pusher object and apollo to the posts component */}
                <Posts pusher={this.pusher} apollo_client={client}/>
              </section>
            </div>
          </ApolloProvider>
        );
      }
    }

    export default App;

Notice that in the snippet above, pusher and apollo_client are passed as properties for the Posts component.

Let’s examine the Posts component.

    // src/components/Posts/index.js

    import React, {Component} from "react";
    import "./Posts.css";
    import gql from "graphql-tag";
    import Post from "../Post";

    class Posts extends Component{
      constructor(){
        super();
        this.state = {
          posts : []
        }
      }
      [...]

In the constructor of the Posts component an array of posts is added to the state of the component.

Then, we use the lifecycle function componentDidMount() to make a query to fetch the existing posts from the server and then set the posts.

    // src/components/Posts/index.js
    [...]

    componentDidMount(){
        // fetch the initial posts 
        this.props.apollo_client
          .query({ 
            query:gql`
              {
                posts(user_id: "a"){
                  id
                  user{
                    nickname
                    avatar
                  }
                  image
                  caption
                }
              } 
            `})
          .then(response => {
            this.setState({ posts: response.data.posts});
          });
          [...]

Subscribe to realtime updates

Next thing is to subscribe the component to the posts-channel and then listen for new-post events:

    // src/components/Posts/index.js
          [...]
          //  subscribe to posts channel
        this.posts_channel = this.props.pusher.subscribe('posts-channel');

        // listen for a new post
        this.posts_channel.bind("new-post", data => {
            this.setState({ posts: this.state.posts.concat(data.post) });
          }, this);
      }
      [...]

Displaying posts

Afterwards, use the render() function to map the posts to the Post component like this:

    // src/components/Posts/index.js
      [...]
       render(){
        return (
          <div className="Posts">
            {this.state.posts.map(post => <Post nickname={post.user.nickname} avatar={post.user.avatar} image={post.image} caption={post.caption} key={post.id}/>)}
          </div>
        );
      }
    }

    export default Posts;

Now, you can go ahead and start your backend server node server and your frontend server npm start. When you navigate to locahost:3000/ you get the following:

Enable desktop notifications for new posts

Now, sometimes users have tabs of applications open but aren’t using them. I’m sure as you’re reading this, you likely have more than one tab open in your web browser - if you’re special, you have > 10. To keep the users engaged, the concepts of notifications was introduced. Developers can now send messages to users based on interaction with the application. Let’s leverage this to keep users notified when a new post has been created.

Checking if notifications are enabled in the browser

Since this feature is fairly new, not all users of your application may have the notification feature on their browser. You need to make a check to see if notifications are enabled. To do this, tweak the src/App.js as follows:

    // src/App.js

    class App extends Component{
      [...]

      componentDidMount(){
        if ('actions' in Notification.prototype) {
          alert('You can enjoy the notification feature');
        } else {
          alert('Sorry notifications are NOT supported on your browser');
        }
      }

      [...]
    }
    export default App;

Requesting permissions

To get started, the first thing you will need to do is to get permission from the user to display notifications. This is put in place so that developers don’t misuse the privilege and begin to spam their users. Edit the src/components/Posts/index.js file as follows :

    // src/components/Posts/index.js

    [...]

    class Posts extends Components{
      [...]

      componentDidMount(){
          // request permission
          Notification.requestPermission();
        [...]

The next thing that needs to be done is to now display the notification to the user when an event is met. This is done by tweaking the this.posts_channel.bind() function :

    // src/components/Posts/index.js

        [...]
          //  subscribe to posts channel
          this.posts_channel = this.props.pusher.subscribe("posts-channel");

          this.posts_channel.bind("new-post", data => {
            // update states
            this.setState({ posts: this.state.posts.concat(data.post) });

            // check if notifcations are permitted
            if(Notification.permission === 'granted' ){
              try{
                // notify user of new post
                new Notification('Pusher Instagram Clone',{ body: `New post from ${data.post.user.nickname}`});
              }catch(e){
                console.log('Error displaying notification');
              }
            }
          }, this);
        }

        render() {
        return (
          <div>
            <div className="Posts">
              {this.state.posts
                .slice(0)
                .reverse()
                .map(post => (
                  <Post
                    nickname={post.user.nickname}
                    avatar={post.user.avatar}
                    image={post.image}
                    caption={post.caption}
                    key={post.id}
                  />
                ))}
            </div>
          </div>
        );
      }
    }

    export default Posts

Now, when you reload your application and head over to localhost:3000/ and you get this:

Interacting with notifications

To add extra functionality, the notification could further be tweaked to allow users to interact with them. To do this, edit the Notification object like this:

    // src/components/Posts/index.js

    // check for notifications 
    if(Notification.permission === 'granted' ){
      try{
        // notify user of new post
        let notification = new Notification(
          'Pusher Instagram Clone',
          { 
            body: `New post from ${data.post.user.nickname}`,
            icon: 'https://img.stackshare.io/service/115/Pusher_logo.png',
            image: `${data.post.image}`,
          }
        );
        // open the website when the notification is clicked
        notification.onclick = function(event){
          window.open('http://localhost:3000','_blank');
        }
      }catch(e){
        console.log('Error displaying notification');
      }
    }

When another user creates a new post, you then get a display that looks like this:

When the user clicks on the notification, they are directed to view the full post.

Conclusion

In this part of the series, we looked at how to incorporate realtime functionality into the instagram-clone application and also saw how to notify users when someone creates new posts using desktop notifications. In the next part of the series, we will see how to take our application offline using service workers. Here’s a link to the full Github repository if interested.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.