Build a realtime Instagram clone — Part 3: Realtime feed updates with Pusher and desktop notifications
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 functionalitybody-parser
andconnect-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.
27 April 2018
by Christian Nwamba