• This repository has been archived on 19/Aug/2020
  • Stars
    star
    235
  • Rank 171,079 (Top 4 %)
  • Language
    JavaScript
  • Created over 6 years ago
  • Updated over 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

In this tutorial, you'll learn how to build a chat app with React, complete with typing indicators, online status, and more.

Build a Slack Clone with React and Pusher Chatkit

Star on GitHub

In this tutorial, you’ll learn how to build a chat app with React and Chatkit.

When we're done, we'll have a chat application complete with typing indicators, a "who's online" list, and message history:

If you think this sounds like a lot to tackle in one tutorial, you'd normally be right!

However, because we'll be using Chatkit, we can more or less focus exclusively on the front-end React code while Chatkit does the heavy lifting.

What is Chatkit?

Chatkit is a hosted API that helps you build impressive chat features into your applications with less code. Features like,

  • Group chat
  • One-to-one chat
  • Private chat
  • Typing indicators
  • "Who's online" presence
  • Read receipts
  • Photo, video, and audio messages

Using our cross-platform SDKs, all chat data is sent to our hosted API where we manage chat state and broadcast it to your clients:

You'll never have to worry about scale or infrastructure, we take care of it all for you.

Perhaps the best way to learn Chatkit is to start building, so I highly recommend you follow along. Along the way, you'll learn best practices when using Chatkit with React.

Steps

This tutorial has been written so that you can follow along, step by step. There are 12 steps in total.

Here's a quick rundown so you know what to expect:

  1. Download the React starter template
  2. Create your own Chatkit instance
  3. Setup a basic Node server
  4. Identifying the user
  5. Render the chat screen
  6. Connect to your Chatkit instance
  7. Create a Chatkit room
  8. Create a basic UI layout
  9. Subscribe to new messages
  10. Sending messages
  11. Add realtime typing indicators
  12. Add a "Who's online" list

Alright, let's code!

Step 1. Download the React starter template

Rather than start from absolute scratch, this walkthrough is based on a minimal starter template:

As you can see, the starter template doesn't contain any interesting logic - just boilerplate we need to run a React application and a simple Node server.

"Server? No one mentioned a server!"

If you're not too familiar with Node, don't worry 😊. After the next section, we won't need to touch the server.

To get started, download the starter template then run npm install:

git clone https://github.com/pusher/build-a-slack-clone-with-react-and-pusher-chatkit chatkit-tutorial
cd chatkit-tutorial
npm install

(Note: To see the completed code, you can view the completed branch or run git checkout complete locally.)

(Also note: This tutorial assumes the use of npm, but the equivalent yarn commands will work as well.)

Step 2. Create your own Chatkit instance

Now you've downloaded the starter template, let's create a Chatkit instance.

To create your own Chatkit instance, head to the dashboard and hit Create new:

Give your instance any name (I called mine "React Chat Tutorial") then take note of your Instance Locator and Secret Key in the Keys tab. We'll need them both in the next section.

Step 3. Setup a basic Node server

While most interactions will happen on the client, Chatkit also needs a server counterpart to create and manage users securely:

We won't authenticate users in this tutorial, but we'll still need to define a route that, when called, creates a Chatkit user.

Start by installing @pusher/chatkit-server:

npm install --save @pusher/chatkit-server

Then update ./server.js:

const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
+const Chatkit = require('@pusher/chatkit-server')

const app = express()

+const chatkit = new Chatkit.default({
+  instanceLocator: 'YOUR INSTANCE LOCATOR',
+  key: 'YOUR KEY',
+})

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(cors())

+app.post('/users', (req, res) => {
+  const { username } = req.body
+  chatkit
+    .createUser({
+      id: username,
+      name: username
+    })
+    .then(() => res.sendStatus(201))
+    .catch(error => {
+      if (error.error === 'services/chatkit/user_already_exists') {
+        res.sendStatus(200)
+      } else {
+        res.status(error.status).json(error)
+      }
+    })
+})

+app.post('/authenticate', (req, res) => {
+  const authData = chatkit.authenticate({ userId: req.query.user_id })
+  res.status(authData.status).send(authData.body)
+})


const PORT = 3001
app.listen(PORT, err => {
  if (err) {
    console.error(err)
  } else {
    console.log(`Running on port ${PORT}`)
  }
})

Remember to replace "YOUR INSTANCE LOCATOR" and "YOUR KEY" with your own respective values.

There's a lot to unpack here, starting from the top:

  • First, we import Chatkit from @pusher/chatkit-server
  • Then, instantiate our own chatkit instance using the Instance Locator and Key we noted in the previous step
  • In the /users route, we take a username and create a Chatkit user through our chatkit instance
  • Authentication is the action of proving a user is who she says she is. When someone first connects to Chatkit, a request will be sent to /authenticate to authenticate them. The server needs to respond with a token (returned by chatkit.authenticate) if the request is valid. In our case, we'll - naïvely - assume that everyone is who they say they are, and return a token from chatkit.authenticate no matter what.

Boom 💥! That's all we need to do on the server. Let's move on to the client...

Step 4. Identifying the user

When someone loads the app, we want to ask them who they are.

Once they hit Submit, we'll send their username to the server (to the /users route we just defined) and create a Chatkit user if one doesn't exist.

To collect the user's name, create a component called UsernameForm.js in in ./src/components/:

+import React, { Component } from 'react'

+class UsernameForm extends Component {
+ constructor(props) {
+   super(props)
+   this.state = {
+     username: '',
+   }
+   this.onSubmit = this.onSubmit.bind(this)
+   this.onChange = this.onChange.bind(this)
+ }

+ onSubmit(e) {
+   e.preventDefault()
+   this.props.onSubmit(this.state.username)
+ }

+ onChange(e) {
+    this.setState({ username: e.target.value })
+  }
+
+  render() {
+    return (
+      <div>
+        <div>
+          <h2>What is your username?</h2>
+          <form onSubmit={this.onSubmit}>
+            <input
+              type="text"
+              placeholder="Your full name"
+              onChange={this.onChange}
+            />
+            <input type="submit" />
+          </form>
+        </div>
+      </div>
+    )
+  }
+}
+
+ export default UsernameForm

Then update App.js:

import React, { Component } from 'react'
+import UsernameForm from './components/UsernameForm'

class App extends Component {
+  constructor() {
+    super()
+    this.state = {
+      currentUsername: '',
+    }
+    this.onUsernameSubmitted = this.onUsernameSubmitted.bind(this)
+  }

+  onUsernameSubmitted(username) {
+    fetch('http://localhost:3001/users', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ username }),
+    })
+      .then(response => {
+        this.setState({
+          currentUsername: username
+        })
+      })
+      .catch(error => console.error('error', error))
+  }

  render() {
-   return <h1>Chatly</h1>
+   return <UsernameForm onSubmit={this.onUsernameSubmitted} />
  }
}

export default App

Run the application using npm start and you'll see that the screen is rendered:

Starting from the top of App.js:

  • First, we import the UsernameForm component. It probably looks familiar to you because it uses a common React pattern called controlled components. You can read more about React forms here
  • In the render function we render the UsernameForm and hook up the onUsernameSubmitted event handler
  • When onUsernameSubmitted is called, we send a POST request to the /users route we just defined. If the request is successful, we update this.state.currentUsername so we can reference it later; otherwise, we console.error the error

Step 5. Render the chat screen

At the moment, we render the UsernameForm and it occupies the entire screen (see the above screenshot).

Once the username has been submitted, we'll want to transition to a different screen - namely, the chat screen.

To do that, we first need to create a ChatScreen.js component in ./src:

+import React, { Component } from 'react'
+
+class ChatScreen extends Component {
+  render() {
+    return (
+      <div>
+        <h1>Chat</h1>
+      </div>
+    )
+  }
+}
+
+export default ChatScreen

Then update App.js:

import React, { Component } from 'react'
import UsernameForm from './components/UsernameForm'
+import ChatScreen from './ChatScreen'

class App extends Component {
  constructor() {
    super()
    this.state = {
      currentUsername: '',
+     currentScreen: 'WhatIsYourUsernameScreen'
    }
    this.onUsernameSubmitted = this.onUsernameSubmitted.bind(this)
 }

  onUsernameSubmitted(username) {
    fetch('http://localhost:3001/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ username }),
    })
      .then(response => {
        this.setState({
          currentUsername: username,
+         currentScreen: 'ChatScreen'
        })
      })
      .catch(error => console.error('error', error))
  }

 render() {
+    if (this.state.currentScreen === 'WhatIsYourUsernameScreen') {
      return <UsernameForm onSubmit={this.onUsernameSubmitted} />
+    }
+    if (this.state.currentScreen === 'ChatScreen') {
+      return <ChatScreen currentUsername={this.state.currentUsername} />
+    }
  }
}

export default App

Rather than use a router, we conditionally render the screen based on this.state.currentScreen.

Step 6. Connect to your Chatkit instance

Earlier, we installed @pusher/chatkit-server. Now we're in client-land, you'll need to install @pusher/chatkit-client as well:

npm install --save @pusher/chatkit-client

Then update ChatScreen.js:

import React, { Component } from 'react'
+import Chatkit from '@pusher/chatkit-client'

class ChatScreen extends Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      currentUser: {}
+    }
+  }

+  componentDidMount () {
+    const chatManager = new Chatkit.ChatManager({
+      instanceLocator: 'YOUR INSTANCE LOCATOR',
+      userId: this.props.currentUsername,
+      tokenProvider: new Chatkit.TokenProvider({
+        url: 'http://localhost:3001/authenticate',
+      }),
+    })
+
+    chatManager
+      .connect()
+      .then(currentUser => {
+        this.setState({ currentUser })
+     })
+     .catch(error => console.error('error', error))
+  }

  render() {
    return (
      <div>
        <h1>Chat</h1>
      </div>
    )
  }
}

export default ChatScreen

Remember to replace "YOUR INSTANCE LOCATOR" with yours that you noted earlier.

Again, starting from the top:

  • First, we import Chatkit
  • Then, instantiate our Chatkit ChatManager with our instanceLocator, userId (from this.props.currentUsername), and a custom TokenProvider. The TokenProvider points to the /authenticate route we defined earlier
  • Once ChatManager has been initialised, we can call connect. connect happens asynchronously and a Promise is returned. If you've followed these steps exactly, you will connect. That being said, watch out for any console.errors in case you missed something

Step 7. Create a Chatkit room

When using Chatkit, all messages are sent to a Chatkit room.

Rooms can be created programmatically (on the server or client using createRoom), or in the dashboard Inspector.

Creating rooms from the Inspector isn't really a good practice (it's mainly intended for testing) but for the purpose of this walkthrough, we'll do it anyway.

In the dashboard, head to the Console tab, where you'll find the Inspector and create a user with any name. I will call mine "Admin".

Then, create a room called "General":

It is really important to note the unique Room id highlighted above.

Step 8. Create a basic UI layout

This step marks a significant point in the walkthrough.

Now we have our boilerplate in place, we can rapidly start to build out chat features.

Going forward, we'll break down each feature into independent (reusable, if you want!) React components:

We will create each component as we go along, but to make the tutorial a bit easier to follow, let's set out the basic component UI layout now:

import React, { Component } from 'react'
import Chatkit from '@pusher/chatkit-client'

class ChatScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      currentUser: {}
    }
  }

  componentDidMount () {
    const chatManager = new Chatkit.ChatManager({
      instanceLocator: 'YOUR INSTANCE LOCATOR',
      userId: this.props.currentUsername,
      tokenProvider: new Chatkit.TokenProvider({
        url: 'http://localhost:3001/authenticate',
      }),
    })

    chatManager
      .connect()
      .then(currentUser => {
        this.setState({ currentUser })
      })
      .catch(error => console.error('error', error))
  }

  render() {
-    return (
-      <div>
-        <h1>Chat</h1>
-      </div>
-    )
+    const styles = {
+      container: {
+        height: '100vh',
+        display: 'flex',
+        flexDirection: 'column',
+      },
+      chatContainer: {
+        display: 'flex',
+        flex: 1,
+      },
+      whosOnlineListContainer: {
+        width: '300px',
+        flex: 'none',
+        padding: 20,
+        backgroundColor: '#2c303b',
+        color: 'white',
+      },
+      chatListContainer: {
+        padding: 20,
+        width: '85%',
+        display: 'flex',
+        flexDirection: 'column',
+      },
+   }

+    return (
+      <div style={styles.container}>
+        <div style={styles.chatContainer}>
+          <aside style={styles.whosOnlineListContainer}>
+            <h2>Who's online PLACEHOLDER</h2>
+          </aside>
+          <section style={styles.chatListContainer}>
+            <h2>Chat PLACEHOLDER</h2>
+          </section>
+        </div>
+      </div>
+    )
  }
}

export default ChatScreen

If you run the app now, you'll see the basic layout take place:

Awesome!

Step 9. Subscribe to new messages

I am really excited to show you this!

Now we have a Chatkit connection, building chat features become as simple as hooking up Chatkit events to UI components. Here, let me show you.

First, create a stateless MessageList.js component in ./src/components:

+ import React, { Component } from 'react'
+
+ class MessagesList extends Component {
+   render() {
+     const styles = {
+       container: {
+         overflowY: 'scroll',
+         flex: 1,
+       },
+       ul: {
+         listStyle: 'none',
+       },
+       li: {
+         marginTop: 13,
+         marginBottom: 13,
+       },
+       senderUsername: {
+         fontWeight: 'bold',
+       },
+       message: { fontSize: 15 },
+     }
+     return (
+       <div
+         style={{
+           ...this.props.style,
+           ...styles.container,
+         }}
+       >
+         <ul style={styles.ul}>
+           {this.props.messages.map((message, index) => (
+             <li key={index} style={styles.li}>
+               <div>
+                 <span style={styles.senderUsername}>{message.senderId}</span>{' '}
+               </div>
+               <p style={styles.message}>{message.text}</p>
+             </li>
+           ))}
+         </ul>
+       </div>
+     )
+   }
+ }
+
+ export default MessagesList

Then update ChatScreen.js:

import React, { Component } from 'react'
import Chatkit from '@pusher/chatkit-client'
+import MessageList from './components/MessageList'


class ChatScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      currentUser: {},
+     currentRoom: {},
+     messages: []
    }
  }

  componentDidMount () {
    const chatManager = new Chatkit.ChatManager({
      instanceLocator: 'YOUR INSTANCE LOCATOR',
      userId: this.props.currentUsername,
      tokenProvider: new Chatkit.TokenProvider({
        url: 'http://localhost:3001/authenticate',
      }),
    })

    chatManager
      .connect()
      .then(currentUser => {
        this.setState({ currentUser })
+        return currentUser.subscribeToRoom({
+          roomId: "YOUR ROOM ID",
+          messageLimit: 100,
+          hooks: {
+            onMessage: message => {
+              this.setState({
+                messages: [...this.state.messages, message],
+              })
+            },
+          },
+        })
+      })
+      .then(currentRoom => {
+        this.setState({ currentRoom })
+       })
      .catch(error => console.error('error', error))
  }

  render() {
    const styles = {
      ...
    }
    return (
      <div style={styles.container}>
        <div style={styles.chatContainer}>
          <aside style={styles.whosOnlineListContainer}>
            <h2>Who's online PLACEHOLDER</h2>
          </aside>
          <section style={styles.chatListContainer}>
-            <h2>Chat PLACEHOLDER</h2>
+            <MessageList
+              messages={this.state.messages}
+              style={styles.chatList}
+            />
          </section>
        </div>
      </div>
    )
  }
}

export default ChatScreen

Remember to replace YOUR ROOM ID with your own room ID that you noted earlier.

Let's break it down:

  • Once you connect to Chatkit you get a currentUser object that represents the current connected user
  • Chatkit is "user-driven" meaning that most if not all interactions happen on the currentUser
  • In this case, we call subscribeToRoom on the currentUser (currentUser.subscribeToRoom)
  • subscribeToRoom takes an event handler called onMessage that is called in real-time each time a new message arrives
  • Because we specified the messageLimit to be 100, onMessage is also called retroactively for up to 100 most recent messages. In practice, this means if you refresh the page you'll see up to 100 of the most recent chat messages
  • There is a fair amount of code here but once you break it down, all we're doing is taking new messages and updating the React state - the significant chat-related code couldn't be more minimal

Step 10. Sending messages

We're on a roll!

Next, let's allow users to send messages by first creating a SendMessageForm.js component in ./src/components:

+ import React, { Component } from 'react'
+
+ class SendMessageForm extends Component {
+   constructor(props) {
+     super(props)
+     this.state = {
+       text: '',
+     }
+     this.onSubmit = this.onSubmit.bind(this)
+     this.onChange = this.onChange.bind(this)
+   }
+
+   onSubmit(e) {
+     e.preventDefault()
+     this.props.onSubmit(this.state.text)
+     this.setState({ text: '' })
+   }
+
+   onChange(e) {
+     this.setState({ text: e.target.value })
+     if (this.props.onChange) {
+       this.props.onChange()
+     }
+   }
+
+   render() {
+     const styles = {
+       container: {
+         padding: 20,
+         borderTop: '1px #4C758F solid',
+         marginBottom: 20,
+       },
+       form: {
+         display: 'flex',
+       },
+       input: {
+         color: 'inherit',
+         background: 'none',
+         outline: 'none',
+         border: 'none',
+         flex: 1,
+         fontSize: 16,
+       },
+     }
+     return (
+       <div style={styles.container}>
+         <div>
+           <form onSubmit={this.onSubmit} style={styles.form}>
+             <input
+               type="text"
+               placeholder="Type a message here then hit ENTER"
+               onChange={this.onChange}
+               value={this.state.text}
+               style={styles.input}
+             />
+           </form>
+         </div>
+       </div>
+     )
+   }
+ }
+
+ export default SendMessageForm

Then - you guessed it - update ChatScreen.js:

import React, { Component } from 'react'
import Chatkit from '@pusher/chatkit-client'
import MessageList from './components/MessageList'
+ import SendMessageForm from './components/SendMessageForm'

class ChatScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      currentUser: {},
      currentRoom: {},
      messages: []
    }
+    this.sendMessage = this.sendMessage.bind(this)
  }


+  sendMessage(text) {
+    this.state.currentUser.sendMessage({
+      text,
+      roomId: this.state.currentRoom.id,
+    })
+  }

 componentDidMount () {
    const chatManager = new Chatkit.ChatManager({
      instanceLocator: 'YOUR INSTANCE LOCATOR',
      userId: this.props.currentUsername,
      tokenProvider: new Chatkit.TokenProvider({
        url: 'http://localhost:3001/authenticate',
      }),
    })

    chatManager
      .connect()
      .then(currentUser => {
        this.setState({ currentUser })
        return currentUser.subscribeToRoom({
          roomId: YOUR ROOM ID,
          messageLimit: 100,
          hooks: {
            onMessage: message => {
              this.setState({
                messages: [...this.state.messages, message],
              })
            },
          },
        })
      })
      .then(currentRoom => {
        this.setState({ currentRoom })
       })
      .catch(error => console.error('error', error))
  }


  render() {
    const styles = {
     ...
    }
    return (
      <div style={styles.container}>
        <div style={styles.chatContainer}>
          <aside style={styles.whosOnlineListContainer}>
            <h2>Who's online PLACEHOLDER</h2>
          </aside>
          <section style={styles.chatListContainer}>
            <MessageList
              messages={this.state.messages}
              style={styles.chatList}
            />
+           <SendMessageForm onSubmit={this.sendMessage} />
          </section>
        </div>
      </div>
    )
  }
}

export default ChatScreen

The SendMessageForm component is essentially the same as theWhatIsYourUsernameForm component we defined earlier.

When the SendMessageForm is submitted, we access this.state.currentUser and call sendMessage (remember, most interactions happen on currentUser)

You can probably see a pattern emerging...

ChatScreen is a container component that manages our application state and renders the UI using presentational - normally stateless - components. Most of our code involves hooking up Chatkit events and their associated data to React UI components.

Step 11. Add realtime typing indicators

If you've ever attempted to implement your own typing indicators, you'll know it can be tricky. In general, more real-time features means more data and more connections to manage.

With Chatkit, you can add typing indicators with little effort.

Start by creating a TypingIndicator.js component in ./src/components:

+import React, { Component } from 'react'
+
+class TypingIndicator extends Component {
+  render() {
+    if (this.props.usersWhoAreTyping.length > 0) {
+      return (
+        <div>
+          {`${this.props.usersWhoAreTyping
+            .slice(0, 2)
+            .join(' and ')} is typing`}
+        </div>
+      )
+    }
+    return <div />
+  }
+}
+
+export default TypingIndicator

Then update ChatScreen.js:

import React, { Component } from 'react'
import Chatkit from '@pusher/chatkit-client'
import MessageList from './components/MessageList'
import SendMessageForm from './components/SendMessageForm'
+import TypingIndicator from './components/TypingIndicator'

class ChatScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      currentUser: {},
      currentRoom: {},
      messages: [],
+     usersWhoAreTyping: [],
    }
    this.sendMessage = this.sendMessage.bind(this)
+   this.sendTypingEvent = this.sendTypingEvent.bind(this)
  }

+  sendTypingEvent() {
+    this.state.currentUser
+      .isTypingIn({ roomId: this.state.currentRoom.id })
+      .catch(error => console.error('error', error))
+  }

  sendMessage(text) {
    this.state.currentUser.sendMessage({
      text,
      roomId: this.state.currentRoom.id,
    })
  }

  componentDidMount() {
    const chatManager = new Chatkit.ChatManager({
      instanceLocator: 'YOUR INSTANCE LOCATOR',
      userId: this.props.currentUsername,
      tokenProvider: new Chatkit.TokenProvider({
        url: 'http://localhost:3001/authenticate',
      }),
    })

    chatManager
      .connect()
      .then(currentUser => {
        this.setState({ currentUser })
        return currentUser.subscribeToRoom({
          roomId: YOUR ROOM ID,
          messageLimit: 100,
          hooks: {
            onMessage: message => {
              this.setState({
                messages: [...this.state.messages, message],
              })
            },
+            onUserStartedTyping: user => {
+              this.setState({
+                usersWhoAreTyping: [...this.state.usersWhoAreTyping, user.name],
+             })
+            },
+            onUserStoppedTyping: user => {
+              this.setState({
+                usersWhoAreTyping: this.state.usersWhoAreTyping.filter(
+                  username => username !== user.name
+                ),
+              })
+            },
          },
        })
      })
      .then(currentRoom => {
        this.setState({ currentRoom })
      })
      .catch(error => console.error('error', error))
  }

  render() {
    const styles = {
      ...
    }
    return (
      <div style={styles.container}>>
        <div style={styles.chatContainer}>
          <aside style={styles.whosOnlineListContainer}>
            <h2>Who's online PLACEHOLDER</h2>
          </aside>
          <section style={styles.chatListContainer}>
            <MessageList
              messages={this.state.messages}
              style={styles.chatList}
            />
+           <TypingIndicator usersWhoAreTyping={this.state.usersWhoAreTyping} />
            <SendMessageForm
              onSubmit={this.sendMessage}
+             onChange={this.sendTypingEvent}
            />
          </section>
        </div>
      </div>
    )
  }
}

export default ChatScreen

When using Chatkit, typing indicators boil down to two fundamental actions:

  • Calling currentUser.userIsTyping when the current user starts typing; then,
  • listening to userStartedTyping and userStoppedTyping events

And that is pretty much it.

"But Alex, what about when the user stops typing?"

That is a very good question.

Chatkit is intelligent like that. If the service doesn't receive a userIsTyping event after a few seconds, it assumes the currentUser has stopped typing. Therefore, there is no need to manually raise an event when someone stops typing. Pretty slick, right?

Step 12. Add a "Who's online" list

Can you feel the momentum? Almost done now 🙌

To finish up the chat app, let's use Chatkit's "who's online" feature to render a list of users and their real-time online status.

Start by creating a WhosOnlineList.js component in /src/components:

+import React, { Component } from 'react'
+
+class WhosOnlineList extends Component {
+  renderUsers() {
+    return (
+      <ul>
+        {this.props.users.map((user, index) => {
+          if (user.id === this.props.currentUser.id) {
+            return (
+              <WhosOnlineListItem key={index} presenceState="online">
+                {user.name} (You)
+              </WhosOnlineListItem>
+            )
+          }
+          return (
+            <WhosOnlineListItem key={index} presenceState={user.presence.state}>
+              {user.name}
+            </WhosOnlineListItem>
+          )
+        })}
+      </ul>
+    )
+  }
+
+  render() {
+    if (this.props.users) {
+      return this.renderUsers()
+    } else {
+      return <p>Loading...</p>
+    }
+  }
+}
+
+class WhosOnlineListItem extends Component {
+  render() {
+    const styles = {
+      li: {
+        display: 'flex',
+        alignItems: 'center',
+        marginTop: 5,
+        marginBottom: 5,
+        paddingTop: 2,
+        paddingBottom: 2,
+      },
+      div: {
+        borderRadius: '50%',
+        width: 11,
+        height: 11,
+        marginRight: 10,
+      },
+    }
+    return (
+      <li style={styles.li}>
+        <div
+          style={{
+            ...styles.div,
+            backgroundColor:
+              this.props.presenceState === 'online' ? '#539eff' : '#414756',
+          }}
+        />
+        {this.props.children}
+      </li>
+    )
+  }
+}
+
+export default WhosOnlineList

Then - for the last time 😢👋 - update ChatScreen.js:

import React, { Component } from 'react'
import Chatkit from '@pusher/chatkit-client'
import MessageList from './components/MessageList'
import SendMessageForm from './components/SendMessageForm'
import TypingIndicator from './components/TypingIndicator'
+import WhosOnlineList from './components/WhosOnlineList'

class ChatScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      currentUser: {},
      currentRoom: {},
      messages: [],
      usersWhoAreTyping: [],
    }
    this.sendMessage = this.sendMessage.bind(this)
    this.sendTypingEvent = this.sendTypingEvent.bind(this)
  }

  sendTypingEvent() {
    this.state.currentUser
      .isTypingIn(this.state.currentRoom.id)
      .catch(error => console.error('error', error))
  }

   sendMessage(text) {
    this.state.currentUser.sendMessage({
      text,
      roomId: this.state.currentRoom.id,
    })
  }

  comonentDidMount() {
    const chatManager = new Chatkit.ChatManager({
      instanceLocator: 'YOUR INSTANCE LOCATOR',
      userId: this.props.currentUsername,
      tokenProvider: new Chatkit.TokenProvider({
        url: 'http://localhost:3001/authenticate',
      }),
    })

    chatManager
      .connect()
      .then(currentUser => {
        this.setState({ currentUser })
        return currentUser.subscribeToRoom({
          roomId: YOUR ROOM ID,
          messageLimit: 100,
          hooks: {
            newMessage: message => {
              this.setState({
                messages: [...this.state.messages, message],
              })
            },
            userStartedTyping: user => {
              this.setState({
                usersWhoAreTyping: [...this.state.usersWhoAreTyping, user.name],
              })
            },
            userStoppedTyping: user => {
              this.setState({
                usersWhoAreTyping: this.state.usersWhoAreTyping.filter(
                  username => username !== user.name
                ),
              })
            },
+            onPresenceChange: () => this.forceUpdate(),
          },
        })
      })
      .then(currentRoom => {
        this.setState({ currentRoom })
      })
      .catch(error => console.error('error', error))
  }

  render() {
    const styles = {
      ...
    }
    return (
      <div style={styles.container}>
        <header style={styles.header}>
          <h2>Chatly</h2>
        </header>
        <div style={styles.chatContainer}>
          <aside style={styles.whosOnlineListContainer}>
-            <h2>Who's online PLACEHOLDER</h2>
+            <WhosOnlineList
+              currentUser={this.state.currentUser}
+              users={this.state.currentRoom.users}
+            />
          </aside>
          <section style={styles.chatListContainer}>
            <MessageList
              messages={this.state.messages}
              style={styles.chatList}
            />
            <TypingIndicator usersWhoAreTyping={this.state.usersWhoAreTyping} />
            <SendMessageForm
              onSubmit={this.sendMessage}
              onChange={this.sendTypingEvent}
            />
          </section>
        </div>
      </div>
    )
  }
}

export default ChatScreen

Managing the state of your users in React state can be a bit tricky so we manage it for you in currentRoom.users.

As users connect and disconnect, this property is dynamically updated. In other words, currentRoom.users should always refelect the current state of your chat app.

Therefore, when users come online or go offline (onPresenceChange), or new users join (onUserAdded) all we have to do is call forceUpdate which tells React to evaluate currentRoom.users and update the UI.

Again, it really boils down to wiring some simple data and events to React components and that's all, folks!

Conclusion

In this walkthrough, you built a complete chat application with

  • group chat;
  • a “Who’s online” list; and,
  • typing indicators

Because we used Chatkit, we also get some bonus features for free:

  • message history (refresh the page and you’ll see up to 100 of the most recent messages);
  • reliability in the case that the client temporarily loses connection (Chatkit handles disconnects gracefully); and,
  • the ability to scale without needing to worry about infrastructure

We wrote a fair amount of code, but none of it was particularly complicated.

Chatkit has a minimal but powerful API that manages all our chat data for us. All we had to do is take that data and render it for the user.

Want to keep building? Why not add rich media support and read receipts? Chatkit supports both:

You may also be interested in checking out our powerful Chatkit Slack demo (250+ stars ⭐️). It’s similar to the application we just built but more complete.

What will you build with Chatkit? We'd love to see! Your feedback guides us in improving Chatkit. Let us know what helps you reach your goals, what’s getting in your way, or what’s missing.

More Repositories

1

pusher-js

Pusher Javascript library
JavaScript
1,970
star
2

atom-pair

An Atom package that allows for epic pair programming
JavaScript
1,454
star
3

pusher-http-php

PHP library for interacting with the Pusher Channels HTTP API
PHP
1,355
star
4

pusher-http-ruby

Ruby library for Pusher Channels HTTP API
Ruby
659
star
5

libPusher

An Objective-C interface to Pusher Channels
C
409
star
6

pusher-http-laravel

[DEPRECATED] A Pusher Channels bridge for Laravel
PHP
405
star
7

pusher-http-python

Pusher Channels HTTP API library for Python
Python
368
star
8

k8s-spot-rescheduler

Tries to move K8s Pods from on-demand to spot instances
Go
313
star
9

pusher-websocket-java

Pusher Channels client library for Java targeting general Java and Android
Java
302
star
10

pusher-websocket-swift

Pusher Channels websocket library for Swift
Swift
267
star
11

pusher-angular

Pusher Angular Library | owner=@leesio
JavaScript
233
star
12

pusher-http-go

Pusher Channels HTTP API library for Go
Go
196
star
13

NWWebSocket

A WebSocket client written in Swift, using the Network framework from Apple.
Swift
123
star
14

k8s-spot-termination-handler

Monitors AWS for spot termination notices when run on spot instances and shuts down gracefully
Makefile
118
star
15

go-interface-fuzzer

Automate the boilerplate of fuzz testing Go interfaces | owner: @willsewell
Go
110
star
16

pusher-http-dotnet

.NET library for interacting with the Pusher HTTP API
C#
109
star
17

pusher-websocket-dotnet

Pusher Channels Client Library for .NET
C#
107
star
18

k8s-auth-example

Example Kubernetes Authentication helper. Performs OIDC login and configures Kubectl appropriately.
Go
107
star
19

faros

Faros is a CRD based GitOps controller
Go
99
star
20

backbone-todo-app

JavaScript
92
star
21

chatkit-client-js

JavaScript client SDK for Pusher Chatkit
JavaScript
90
star
22

pusher-channels-flutter

Pusher Channels client library for Flutter targeting IOS, Android, and WEB
Dart
72
star
23

quack

In-Cluster templating for Kubernetes manifests
Go
70
star
24

pusher-websocket-react-native

React Native official Pusher SDK
TypeScript
61
star
25

websockets-from-scratch-tutorial

Tutorial that shows how to implement a websocket server using Ruby's built-in libs
Ruby
60
star
26

push-notifications-php

Pusher Beams PHP Server SDK
PHP
56
star
27

backpusher

JavaScript
54
star
28

chatkit-android

Android client SDK for Pusher Chatkit
Kotlin
53
star
29

django-pusherable

Real time notification when an object view is accessed via Pusher
Python
52
star
30

cli

A CLI for Pusher (beta)
Go
51
star
31

notify

Ruby
51
star
32

k8s-spot-price-monitor

Monitors the spot prices of instances in a Kubernetes cluster and exposes them as prometheus metrics
Python
44
star
33

chatkit-command-line-chat

A CLI chat, built with Chatkit
JavaScript
41
star
34

pusher-http-java

Java client to interact with the Pusher HTTP API
Java
40
star
35

chatkit-swift

Swift SDK for Pusher Chatkit
Swift
40
star
36

push-notifications-web

Beams Browser notifications
JavaScript
39
star
37

electron-desktop-chat

A desktop chat built with React, React Desktop and Electron
JavaScript
38
star
38

crank

Process slow restarter
Go
37
star
39

pusher-websocket-android

Library built on top of pusher-websocket-java for Android. Want Push Notifications? Check out Pusher Beams!
Java
35
star
40

chameleon

A collection of front-end UI components used across Pusher ✨
CSS
35
star
41

chatkit-server-php

PHP SDK for Pusher Chatkit
PHP
35
star
42

push-notifications-swift

Swift SDK for the Pusher Beams product:
Swift
34
star
43

cide

Isolated test runner with Docker
Ruby
33
star
44

pusher-phonegap-android

JavaScript
30
star
45

push-notifications-python

Pusher Beams Python Server SDK
Python
30
star
46

pusher-websocket-unity

Pusher Channels Unity Client Library
C#
27
star
47

hacktoberfest

24
star
48

laravel-chat

PHP
23
star
49

push-notifications-android

Android SDK for Pusher Beams
Kotlin
21
star
50

push-notifications-node

Pusher Beams Node.js Server SDK
JavaScript
20
star
51

pusher-test-iOS

iOS app for developers to test connections to Pusher
Objective-C
19
star
52

push-notifications-ruby

Pusher Beams Ruby Server SDK
Ruby
18
star
53

chatkit-server-node

Node.js SDK for Pusher Chatkit
TypeScript
16
star
54

rack-headers_filter

Remove untrusted headers from Rack requests | owner=@zimbatm
Ruby
15
star
55

pusher-test-android

Test and diagnostic app for Android, based on pusher-java-client
Java
14
star
56

pusher-realtime-tfl-cameras

Realtime TfL Traffic Camera API, powered by Pusher
JavaScript
14
star
57

buddha

Buddha command execution and health checking | owner: @willsewell
Go
14
star
58

chatkit-server-go

Chatkit server SDK for Golang
Go
13
star
59

pusher-channels-auth-example

A simple server exposing a pusher auth endpoint
JavaScript
13
star
60

pusher-platform-js

Pusher Platform client library for browsers and react native
TypeScript
13
star
61

stronghold

[DEPRECATED] A configuration service | owner: @willsewell
Haskell
12
star
62

sample-chatroom-ios-chatkit

How to make an iOS Chatroom app using Swift and Chatkit
PHP
12
star
63

pusher-twilio-example

CSS
12
star
64

chatkit-server-ruby

Ruby server SDK for Chatkit
Ruby
12
star
65

prom-rule-reloader

Watches configmaps for prometheus rules and keeps prometheus in-sync
Go
12
star
66

electron-desktop-starter-template

JavaScript
11
star
67

realtime-visitor-tracker

Realtime location aware visitor tracker for a web site or application
PHP
11
star
68

push-notifications-server-java

Pusher Beams Java Server SDK
Kotlin
10
star
69

android-slack-clone

Android chat application, built with Chatkit
Kotlin
10
star
70

filtrand

JavaScript
10
star
71

vault

Front-end pattern library
Ruby
9
star
72

git-store

Go git abstraction for use in Kubernetes Controllers
Go
9
star
73

pusher-platform-android

Pusher Platform SDK for Android
Kotlin
9
star
74

push-notifications-go

Pusher Beams Go Server SDK
Go
9
star
75

pusher-platform-swift

Swift SDK for Pusher platform products
Swift
8
star
76

realtime_survey_complete

JavaScript
8
star
77

docs

The all new Pusher docs, powered by @11ty and @vercel
CSS
8
star
78

push-notifications-server-swift

Pusher Beams Swift Server SDK
Swift
8
star
79

pusher-python-rest

Python client to interact with the Pusher REST API. DEPRECATED in favour of https://github.com/pusher/pusher-http-python
Python
8
star
80

real-time-progress-bar-tutorial

Used inthe realtime progress bar tutorial blog post - http://blog.pusher.com
JavaScript
7
star
81

pusher-channels-chunking-example

HTML
7
star
82

pusher-http-swift

Swift library for interacting with the Pusher Channels HTTP API
Swift
7
star
83

feeds-client-js

JS client for Pusher Feeds
JavaScript
6
star
84

pusher-test

Simple website which allows manual testing of pusher-js versions
JavaScript
6
star
85

java-websocket

A fork of https://github.com/TooTallNate/Java-WebSocket | owner=@zmarkan
HTML
6
star
86

navarchos

Node replacing controller
Go
5
star
87

bridge-troll

A Troll that ensures files don't change
Go
5
star
88

realtime-notifications-tutorial

Create realtime notifications in minutes, not days =)
4
star
89

pusher-socket-protocol

Protocol for pusher sockets
HTML
4
star
90

icanhazissues

Github issues kanban
JavaScript
4
star
91

textsync-server-node

[DEPRECATED] A node.js library to simplify token generation for TextSync authorization endpoints.
TypeScript
4
star
92

pusher_tutorial_realtimeresults

JavaScript
3
star
93

pusher-js-diagnostics

JavaScript
3
star
94

react-rest-api-tutorial

Accompanying tutorial for consuming RESTful APIs in React
CSS
3
star
95

feeds-server-node

The server Node SDK for Pusher Feeds
JavaScript
3
star
96

testing

Configuration for Pusher's Open Source Prow instance
Go
3
star
97

spacegame_example

Simple example of a space game using node.js and Pusher
JavaScript
3
star
98

chatkit-quickstart-swift

A project to get started with Chatkit.
Swift
2
star
99

pusher-whos-in

Ruby
2
star
100

healthz-proxy

healthz proxy for zero downtime rollouts
Go
2
star