Creating a chat app with React Native and Chatkit

  • Wern Ancheta
October 10th, 2018
You will need basic knowledge of React Native. You will need two devices to test the app - this tutorial uses Expo for this purpose.

In this tutorial, we’ll build a simple chat app with React Native and Chatkit.

Chatkit is a Pusher service that allows Android, Swift, React Native, and web developers to easily implement various chat features such as message storage, typing indicators, and online presence.

Prerequisites

Basic knowledge of React Native is required in order to follow along.

We’ll be specifically using Expo to develop the app as it makes it easier to test it on a device. This is specifically useful since we need at least two devices in order to fully test the app. If your machine is not that powerful, it will basically struggle if you run two simulator instances so running it on a device will offset some of that processing power and memory consumption.

If you already have the React Native development environment set up, you can use it as well as we won’t be using any Expo-specific components.

Lastly, you need the Expo client app for either Android or iOS. This allows us to easily run the app on our device.

App overview

Aside from the realtime chat, the app will have the following features:

  • View the users who are currently online.
  • Typing indicator when the user you’re chatting with is currently typing something.
  • Loading of older messages.

Here’s what the final output will look like:

You can view the source code of the app in this GitHub repo.

Setting up a Chatkit app

If you haven’t done so already, create a Pusher account and log in to it. Once logged in, you’ll be greeted with the following screen:

Click on the Create button inside the Chatkit card, enter the name of the app:

Take note of the credentials as we will be needing them later.

Next, scroll down to the Instance Inspector section and create a new user:

Creating a user allows us to inspect the users, rooms, and roles set for each user. This will be useful to us later on as we will be able to create a room and let the selected user join it:

Next, enable the test token provider. This allows us to use Pusher’s servers to generate a token. This token is used for authenticating the user so they can use Chatkit:

Note that this is only useful for testing purposes, don’t use Pusher’s servers for production apps.

Building the app

Before we proceed, first make sure you have the latest version of Expo installed by re-installing the Expo command line tool:

    npm install -g expo-cli

The following package versions are used in building the app:

  • Node 8.3.0
  • Yarn 1.7.0
  • Expo CLI 1.2.3
  • Expo SDK 29.0.0
  • Chatkit 0.7.9
  • React 16.3.1

Now we’re ready to build the app. Start by cloning the GitHub repo and switching to the starter branch:

    git clone https://github.com/anchetaWern/RNChatkit.git
    cd RNChatkit
    git checkout RNChatkit

The starter branch contains all the files that we will be using for the project, except for the server-related files. For now, they only contain minimal UI and all of the styles. We will be updating these files as we move along.

Next, we install the dependencies. In this case, we only have the Chatkit library. We will use it to connect to Chatkit:

    yarn install

Login screen

Start by updating the login screen. All we need here is a text input for entering the username and a button to log in. The function for logging in will be passed via props from the App.js file later on:

    // app/screens/Login.js
    import React from 'react';
    import { StyleSheet, Text, View, TextInput, Button } from 'react-native'; // import all the necessary components

    export default class Login extends React.Component {
      // replace existing contents with this one
      render() {
        return (
          <View>
            <Text style={styles.label}>Username</Text>
            <TextInput
              style={styles.text_field}
              onChangeText={this.props.updateUsername}
              value={this.props.username}
              placeholder="Enter your username"
            />

            <Button onPress={this.props.enterChat} title="Enter" color="#0064e1" />
          </View>
        );
      }
    }

    // previously added styles here...

Users screen

The Users screen is responsible for listing all the users that are currently online. This is where the user can select someone to chat with. They can also log out of the app from this screen.

Since it will take a few seconds for the app to load the online users, we show an activity indicator by default and only hide it once all the data has been loaded:

    // app/screens/Users.js
    import React from 'react';
    import {
      StyleSheet,
      Text,
      View,
      // import these:
      FlatList,
      TouchableHighlight,
      TouchableOpacity,
      ActivityIndicator,
    } from 'react-native';

    import Header from '../components/Header';

    export default class Users extends React.Component {
      // replace existing contents with this one
      render() {
        return (
          <View style={styles.container}>
            <Header text="Users">
              {this.props.userHasLoggedIn && (
                <TouchableOpacity onPress={this.props.leavePresenceRoom}>
                  <View style={styles.leave_button}>
                    <Text style={styles.leave_button_text}>Logout</Text>
                  </View>
                </TouchableOpacity>
              )}
            </Header>

            <View style={styles.body}>
              {this.props.users.length == 0 && (
                <View style={styles.activity}>
                  <ActivityIndicator size="large" color="#05a5d1" />
                  <Text style={styles.activity_text}>Loading users...</Text>
                </View>
              )}

              {this.props.users.length > 0 && (
                <FlatList
                  data={this.props.users}
                  renderItem={this.renderItem}
                  keyExtractor={(item) => {
                    return item.id.toString();
                  }}
                />
              )}
            </View>
          </View>
        );
      }

      // next: add renderItem
    }

    // previously added styles here...

Here’s the function for rendering each list item. Note that a user isn’t removed from the list when they go offline or idle, so we need a way to indicate which users are offline or online, that’s why we have the online_style. The Chatkit API exposes an is_online field for each user data, and it’s what we’re using to determine if the user is online or offline. Each list item can be clicked to begin a chat with that user. We’ll be passing this function as props from the App.js file later on:

    // app/screens/Users.js
    renderItem = ({ item }) => {
      let online_style = item.is_online ? 'online' : 'offline';

      return (
        <TouchableHighlight
          onPress={() => {
            console.log('now beginning chat...');
            this.props.beginChat(item);
          }}
          underlayColor="#f3f3f3"
          style={styles.list_item}>
          <View style={styles.list_item_body}>
            <View style={[styles.online_indicator, styles[online_style]]} />

            <Text style={styles.username}>{item.name}</Text>
          </View>
        </TouchableHighlight>
      );
    };

Chat screen

The Chat screen is where we add the actual chat UI. It contains the list of messages sent by either of the users, a text field for entering a message, and a button for sending it. We also have a refresh control which allows the user to load the previous messages that were sent in that chat room via a “pull to refresh” gesture. Start by importing all the components that we need:

    // app/screens/Chat.js
    import React from 'react';
    import {
      StyleSheet,
      Text,
      View,
      ScrollView,
      FlatList,
      TextInput,
      TouchableOpacity,
      KeyboardAvoidingView,
      RefreshControl,
    } from 'react-native';

    import Header from '../components/Header';

Next, render the chat UI. We’re wrapping everything in a KeyboardAvoidingView so that the entire chat UI will adjust upwards to avoid the keyboard UI while the user is typing their message. The Header shows the name of the user they’re chatting with, and a button to leave the chat room. Again, all the chat-related data and functions are passed from the App.js file.

After the header, we wrap the message list, refresh control, typing indicator, and the form for sending a message in a single view:

    // app/screens/Chat.js
    export default class Chat extends React.Component {
      render() {
        const { refreshing = false } = this.props;

        return (
          <KeyboardAvoidingView style={styles.container} behavior="padding" enabled>
            <Header text={this.props.chatWithUser}>
              {this.props.inChatRoom && (
                <TouchableOpacity onPress={this.props.backToUsers}>
                  <View style={styles.leave_button}>
                    <Text style={styles.leave_button_text}>Leave</Text>
                  </View>
                </TouchableOpacity>
              )}
            </Header>

            <View style={styles.body}>
              <ScrollView
                style={styles.messages}
                contentContainerStyle={styles.scroll_container}
                ref={this.props.setScrollViewRef}
                refreshControl={
                  <RefreshControl
                    refreshing={this.props.refreshing}
                    onRefresh={this.props.loadPreviousMessages}
                  />
                }>
                <FlatList data={this.props.messages} renderItem={this.renderItem} />
              </ScrollView>

              {this.props.chatWithUserIsTyping && (
                <View style={styles.typing_indicator}>
                  <Text style={styles.typing_indicator_text}>
                    {this.props.chatWithUser} is typing...
                  </Text>
                </View>
              )}

              <View style={styles.message_box}>
                <TextInput
                  style={styles.text_field}
                  multiline={true}
                  onChangeText={this.props.updateMessage}
                  value={this.props.message}
                  placeholder="Type your message..."
                />

                <View style={styles.button_container}>
                  {this.props.inChatRoom && (
                    <TouchableOpacity onPress={this.props.sendMessage}>
                      <View style={styles.send_button}>
                        <Text style={styles.send_button_text}>Send</Text>
                      </View>
                    </TouchableOpacity>
                  )}
                </View>
              </View>
            </View>
          </KeyboardAvoidingView>
        );
      }

      // next: add renderItem  
    }

    // previously added styles here..

If you’re wondering what this.props.setScrollViewRef is, it’s the reference to the scroll view. In React Native, refs are usually used to manipulate a specific component. For a scroll view, a ref is usually used to programmatically scroll the scroll view to the bottom of the screen. And that’s exactly what we’re using it for: to scroll to the bottom of the screen when a new message arrives. This allows the user to easily view the latest message.

Next is the function for rendering a single message instance. This displays the username of the user who sent the message as well as their actual message. We change the styles of the container depending on whether the current user sent the message or not. All the messages sent by the current user will be displayed on the right side, and its container will have a blue background. While all other messages will be displayed on the left side with a gray background. The username is only displayed if the current user didn’t send the message:

    // app/screens/Chat.js
    renderItem = ({ item }) => {
      let box_style = item.isCurrentUser ? 'current_user_msg' : 'other_user_msg';
      let username_style = item.isCurrentUser
        ? 'current_user_username'
        : 'other_user_username';

      return (
        <View key={item.key} style={styles.msg}>
          <View style={styles.msg_wrapper}>
            <View style={styles.username}>
              <Text style={[styles.username_text, styles[username_style]]}>
                {item.username}
              </Text>
            </View>
            <View style={[styles.msg_body, styles[box_style]]}>
              <Text style={styles[`${box_style}_text`]}>{item.msg}</Text>
            </View>
          </View>
        </View>
      );
    };

Creating the server

Before we hook everything up in the App.js file, we first need to create the server.

First, create a server directory in the root of the project directory. Inside the directory, create a package.json file and add the following:

    {
      "name": "chatserver",
      "version": "1.0.0",
      "description": "",
      "main": "server.js",
      "scripts": {
        "start": "node server.js"
      },
      "dependencies": {
        "@pusher/chatkit-server": "^0.12.1",
        "body-parser": "^1.17.2",
        "cors": "^2.8.4",
        "express": "^4.16.3"
      }
    }

Save the file and execute npm install to install the packages. Here’s a breakdown of what each one does:

  • @pusher/chatkit-server - the Node.js SDK for Chatkit. This allows us to create new users for the Chatkit instance we created earlier. This happens when someone logs in to the app. The server creates a new user if the username used doesn’t already exist.
  • body-parser - allows us to parse the request body when someone makes a login request.
  • cors - enables CORS (Cross-Origin Resource Sharing) in the server so that requests coming from the app won’t get rejected. The app and the server doesn’t live in the same origin or host so requests get rejected by default if we don’t enable CORS.
  • express - allows us to easily spin up an HTTP server and use middlewares such as cors and body-parser.

Next, create a server.js file. Start by importing the packages we just installed:

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

Initialize the Express server and Chatkit. Copy the instance locator ID and Chatkit secret from the Chatkit app instance you created earlier (Note: omit the v1:us1: prefix from the instance locator, and copy the secret key as is):

    const app = express();
    const instance_locator_id = 'YOUR INSTANCE LOCATOR ID';
    const chatkit_secret = 'YOUR CHATKIT SECRET';

    const chatkit = new Chatkit.default({
      instanceLocator: `v1:us1:${instance_locator_id}`,
      key: chatkit_secret,
    });

Use the body parser and CORS middleware:

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

Add a route for verifying if the server is working. If it says “all green!” when you access this route from the browser, the server works. We’ll do it later once we run the server:

    app.get('/', (req, res) => {
      res.send('all green!');
    });

Next, add the route for logging users in. The app includes the username of the user in the request body when it makes a request to this route. That’s what we’re extracting from the request body. We then use this username as a value for both the id and name required by Chatkit when creating users. If the request succeeded, we return a 201 created response. If it didn’t succeed, it either means that the user already exists or something went wrong with the request. It’s totally fine if the user already exists, we don’t really have a functionality to check whether the user is logging in with the same username and device combination:

    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 {
            let statusCode = error.status;
            if (statusCode >= 100 && statusCode < 600) {
              res.status(statusCode);
            } else {
              res.status(500);
            }
          }
        });
    });

Lastly, start the server at port 3000:

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

At this point, you should now be able to run the server:

    node server.js

Access http://localhost:3000 on your browser to test if the server is working.

Bringing everything together

Now that you have added the UI for all the screens and started the server, all we have to do is hook everything up in the App.js file. As mentioned earlier, all the relevant app data and function is passed from the App.js file to each of the screens. This allows us to only have all the Chatkit-related initialization in a single file.

Start by importing the packages and screens that we need. From the Chatkit package, we only need the ChatManager which is where all the Chatkit methods are stored, and the TokenProvider which we use to initialize a new TokenProvider instance. This TokenProvider allows us to connect to the Chatkit service via an auth token generated from a server. In this case, we’re using a server provided by Pusher to generate a token that we can use to make requests to the Chatkit API:

    // App.js
    import React from "react";
    import { StyleSheet, Text, View } from "react-native";
    import { ChatManager, TokenProvider } from "@pusher/chatkit";

    import Login from "./app/screens/Login";
    import Users from "./app/screens/Users";
    import Chat from "./app/screens/Chat";

Next, initialize the TokenProvider. This requires us to enter the instance locator ID (don’t forget to remove the v1:us1: prefix from this one as well) and the ID of the general chat room (note that this should be an integer, so you shouldn’t wrap it in quotes):

    const instanceLocatorId = "YOUR INSTANCE LOCATOR ID";
    const presenceRoomId = THE_ID_OF_A_GENERAL_ROOM; // room ID of the general room created through the chatKit inspector

    const tokenProvider = new TokenProvider({
      url: `https://us1.pusherplatform.io/services/chatkit_token_provider/v1/${instanceLocatorId}/token`
    });

You can create a general room in the Chatkit dashboard. Under the Instance Inspector section, click on the ROOMS tab. From there, click CREATE NEW ROOM. This opens a modal that lets you pick the name of the user to use to create the room, and the actual name of the room:

Once the room is created, the room’s ID will be shown below its name. Use this ID as the value for the presenceRoomID from earlier:

In Chatkit, a single app instance can have many chat rooms. These chat rooms enable us to have a conversation between specific users only, instead of all the users who are currently logged in. The general chat room gives a space for us to determine the online status of all the users who are logged in (either online or idle/offline).

When a user logs in, they’re automatically assigned to the general room, that way, all the other users who have previously logged in can see if there’s a new user whom they can chat with. The general room is strictly for this purpose only, the users in that general room cannot actually chat with one another in a group chat fashion. The only thing they can do is select a logged in user from the list so they could chat with them.

Next, get the internal IP (the one assigned by your router) of your computer and replace the placeholder value (YOUR_INTERNAL_IP) below:

    const chatServer = "http://YOUR_INTERNAL_IP:3000/users";

If you want to test the app with someone outside your home network, you can also use ngrok to expose the server, then use the https URL that ngrok is exposing instead.

Now we’re ready to add the main app code. Start by initializing the state, these are the values which we’ll constantly be updating throughout the lifecycle of the app. In the constructor, we also have a few values for keeping track of the currently logged in user, the ID of the room which you’re in, and the user you’re chatting with:

    export default class App extends React.Component {
      state = {
        userHasLoggedIn: false, // whether the user is logged in or not
        currentScreen: "login", // the current screen being shown, this defaults to the login screen
        username: null, // the username of the current user
        users: [], // the array of users returned by Chatkit
        presenceRoomId: null, // the ID of the general room (we're simply copying it over to the state)
        currentRoomId: null, // the ID of the current room
        chatWithUser: null, // the username of the user you're currently chatting with
        message: "", // the message you're currently typing
        messages: [], // the array of messages currently being shown in the screen
        chatWithUserIsTyping: false, // if the user you're chatting with is currently typing something or not
        refreshing: false, // if the app is currently fetching the old messages or not
        inChatRoom: false // if you're currently in a chat room or not
      };

      constructor(props) {
        super(props);
        this.currentUser = null;
        this.roomId = null;
        this.chatWithUser = null;
      }

      // next: add render method

    }

Next, in the render method, we show either the Login screen, Users screen or the Chat screen based on the value of the currentScreen. This is also where we pass the props that we were using earlier in each of the screens. You already know what each of the state items is for (they’re in the comments in the code block above), but for the functions, we’ll create each of them shortly:

    render() {
      return (
        <View style={styles.container}>
          {this.state.currentScreen == "login" && (
            <Login
              username={this.state.username}
              updateUsername={this.updateUsername}
              enterChat={this.enterChat}
            />
          )}

          {this.state.currentScreen == "users" && (
            <Users
              userHasLoggedIn={this.state.userHasLoggedIn}
              users={this.sortUsers(this.state.users)}
              beginChat={this.beginChat}
              leavePresenceRoom={this.leavePresenceRoom}
            />
          )}

          {this.state.currentScreen == "chat" && (
            <Chat
              message={this.state.message}
              backToUsers={this.backToUsers}
              updateMessage={this.updateMessage}
              sendMessage={this.sendMessage}
              chatWithUser={this.state.chatWithUser}
              chatWithUserIsTyping={this.state.chatWithUserIsTyping}
              messages={this.state.messages}
              refreshing={this.state.refreshing}
              loadPreviousMessages={this.loadPreviousMessages}
              setScrollViewRef={this.setScrollViewRef}
              inChatRoom={this.state.inChatRoom}
            />
          )}
        </View>
      );
    }

    // next: add updateUsername function

The following are the functions that we’re passing to the Login screen.

The updateUsername function is used for updating the value of the text field for entering the username:

    updateUsername = username => {
      this.setState({
        username
      });
    };

    // next: add enterChat function

The enterChat function handles logging the user in and subscribing the user to the general room. Here’s a break down of what this function does:

  1. Make a request to the server we created earlier to log the user in. Here, we use the fetch method to make a request.
  2. If we get either a 200 or 201 response from the server, the function that we passed inside then() will be executed.
  3. Next, we initialize a ChatManager instance and use it to connect the user to ChatKit.
  4. Once the user is connected, we subscribe to the general room. If you’re wondering what the difference between subscribing to a room and joining a room is, it’s is in how they work. Joining a room simply allows the user to join a room. This allows them to send a message to the room, that’s all. On the other hand, subscribing to a room implicitly joins the user the room while also allowing them to subscribe to certain hooks (for example, onNewMessage, onUserStartedTyping, onUserWentOffline) and execute a specific function when the hook is triggered by Chatkit. So if all you need is send messages, use the joinRoom method.
  5. After subscribing to the general room, the Chatkit API returns all the relevant information of the general room. One of those info is contained in room.users which is used to get information about the users who have joined the general room. All we really need to get is their user ID, username, and online status.
  6. Once the variable containing the array of user objects is updated, we add it to the state and set the current screen to the Users screen.

Here’s what it looks like in code:

    enterChat = () => {
      fetch(chatServer, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          username: this.state.username
        })
      })
        .then(response => { // request succeeded
          // initialize a ChatManager instance
          this.chatManager = new ChatManager({
            instanceLocator: `v1:us1:${instanceLocatorId}`,
            userId: this.state.username,
            tokenProvider
          });

          // connect the user to Chatkit
          this.chatManager
            .connect()
            .then(currentUser => {
              this.currentUser = currentUser;

              this.setState({
                presenceRoomId: presenceRoomId // save ID of the general room in the state
              });

              // subscribe the user to the general room
              currentUser
                .subscribeToRoom({
                  roomId: presenceRoomId,
                  // action hooks. These functions will be executed when any of the four events below happens
                  hooks: {
                    onUserCameOnline: this.handleInUser,
                    onUserJoinedRoom: this.handleInUser,
                    onUserLeftRoom: this.handleOutUser,
                    onUserWentOffline: this.handleOutUser
                  }
                })
                .then(room => {
                  let new_users = [];
                  room.users.forEach(user => {
                    if (user.id != this.currentUser.id) {
                      let is_online =
                        user.presence.state == "online" ? true : false;

                      new_users.push({
                        id: user.id,
                        name: user.name,
                        is_online
                      });
                    }
                  });

                  this.setState({
                    userHasLoggedIn: true,
                    users: new_users
                  });
                })
                .catch(err => {
                  console.log(`Error joining room ${err}`);
                });
            })
            .catch(error => {
              console.log("error with chat manager", error);
            });
        })
        .catch(error => {
          console.log("error in request: ");
        });

      this.setState({
        currentScreen: "users"
      });
    };

    // next: add handleInUser function

The handleInUser function is executed when either of the onUserCameOnline and onUserJoinedRoom hook is triggered by Chatkit. These hooks are triggered when a user that was previously idle came online and when a new user has joined the general room. All we’re doing here is setting the is_offline property of the user who just went online to true so that their online indicator will be colored green instead of gray:

    handleInUser = user => {
      let currentUsers = [...this.state.users];
      let userIndex = currentUsers.findIndex(item => item.id == user.id);

      if (userIndex != -1) {
        currentUsers[userIndex]["is_online"] = true;
      }

      if (user.id != this.currentUser.id && userIndex == -1) {
        currentUsers.push({
          id: user.id,
          name: user.name,
          is_online: true
        });
      }

      this.setState({
        users: currentUsers
      });
    };
    // next: add sortUsers function

The sortUsers function is used for sorting the currently logged in users based on who is online. So those who are online will be listed first, and the idle one’s will be last:

    sortUsers = users => {
      return users.slice().sort((x, y) => {
        return y.is_online - x.is_online;
      });
    };
    // next: add handleOutUser function

The handleOutUser function is executed when either the onUserLeftRoom or onUserWentOffline hook is triggered by Chatkit. This is where we update the is_online property of a user to false. This function is executed when any of the users in the general room logs out or they went idle:

    handleOutUser = user => {
      let users = [...this.state.users];
      let new_users = users.filter(item => {
        if (item.id == user.id) {
          item.is_online = false;
        }
        return item;
      });

      this.setState({
        users: new_users
      });
    };
    // next: add beginChat function

Next, the beginChat function is executed when the user clicks on a user listed on the Users screen. Note that it doesn’t matter if the user they’re trying to chat with is idle, as long as its listed, chatting is possible. There’s quite a bit going on in this function, so I’ll try to break it down:

  1. The room ID is constructed based on the username of the user you’re trying to chat with and your username. So if my name is “johnny”, and I’m trying to chat with a user named “bravo”, the room name would be bravo_johnny_room. The usernames are arranged alphabetically so it doesn’t matter who initiated the conversation, the room name would still be the same.
  2. Call the getJoinableRooms function. This allows us to fetch the rooms that the current user is able to join. Note that this doesn’t include rooms that the user is currently a member of. That’s why it’s important for a user to leave the chat room once they’re done chatting because getJoinableRooms won’t return that room since they’re still officially a member of that room.
  3. If a chat room that has the same name as the room name constructed in step one doesn’t exist in the joinable rooms, we create a new room with that room name. Once created, we call the subscribeToRoom function to add all the necessary subscription hooks. This is where another problem arises if a user didn’t explicitly leave a chat room. The problem is that the same room name is used, thus the user you previously chatted with in the room with the same name is actually using the old instance while you’re now using the new instance. And if that user also didn’t leave the room then a new room with the same name will also be created. The createRoom function doesn’t actually check if the room name you used is unique to that Chatkit app instance.
  4. If the chat room was found, we simply subscribe to the room.

Here’s the code:

    beginChat = user => {
      // construct the room ID
      let roomName = [user.id, this.currentUser.id];
      roomName = roomName.sort().join("_") + "_room";

      this.currentUser
        .getJoinableRooms()
        .then(rooms => {
          var chat_room = rooms.find(room => {
            return room.name == roomName;
          });

          if (!chat_room) {
            this.currentUser
              .createRoom({
                name: roomName,
                private: false // so they could find it in joinable rooms
              })
              .then(room => {
                this.subscribeToRoom(room.id, user.id);
              })
              .catch(err => {
                console.log(`error creating room ${err}`);
              });
          } else {
            this.subscribeToRoom(chat_room.id, user.id);
          }
        })
        .catch(err => {
          console.log(`error getting joinable rooms: ${err}`);
        });
    };
    // next: add subscribeToRoom function

As you’ve seen above, the subscribeToRoom function is called when the user begins to chat with another user. The purpose of this function is to subscribe the user to the room. Subscribing allows us to subscribe to room subscriptions hooks. In this case, we only need to listen for three events: onNewMessage, onUserStartedTyping, and onUserStoppedTyping. We’ll be declaring the functions to execute in each of these instances shortly:

    subscribeToRoom = (roomId, chatWith) => {
      this.roomId = roomId;
      this.chatWithUser = chatWith;

      this.currentUser
        .subscribeToRoom({
          roomId: roomId,
          hooks: {
            onNewMessage: this.onReceiveMessage,
            onUserStartedTyping: this.onUserTypes,
            onUserStoppedTyping: this.onUserNotTypes
          },
          messageLimit: 5 // default number of messages to load after subscribing to the room
        })
        .then(room => {
          this.setState({
            inChatRoom: true
          });
          console.log(`successfully subscribed to room`);
        })
        .catch(err => {
          console.log(`error subscribing to room: ${err}`);
        });

      this.setState({
        currentScreen: "chat", // set current screen to Chat screen
        currentRoomId: roomId,
        chatWithUser: chatWith
      });
    };
    // next: add onReceiveMessage function

The onReceiveMessage function is executed when a new message is received in the chat room. It doesn’t matter if the current user sent it or not, this function gets executed regardless of who sent the message. Here, we append the new message to the existing array of messages in the state. Once the state is updated, we use the reference to the ScrollView to scroll to the end of the list:

    onReceiveMessage = message => {
      let isCurrentUser = this.currentUser.id == message.sender.id ? true : false;

      let messages = [...this.state.messages];
      messages.push({
        key: message.id.toString(),
        username: message.sender.name,
        msg: message.text,
        datetime: message.createdAt,
        isCurrentUser // this one determines the styling used for the chat bubble
      });

      this.setState(
        {
          messages
        },
        () => {
          this.scrollViewRef.scrollToEnd({ animated: true });
        }
      );
    };

    // next: add onUserTypes function

The onUserTypes function is executed when the onUserStartedTyping hook is triggered. While the onUserNotTypes function is executed, when the onUserStoppedTyping hook is triggered. Both functions simply updates the state to either show or hide the “username is typing..” message:

    onUserTypes = user => {
      this.setState({
        chatWithUserIsTyping: true
      });
    };

    onUserNotTypes = user => {
      this.setState({
        chatWithUserIsTyping: false
      });
    };

    // next: add backToUsers function 

The backToUsers function is executed when a user leaves a chat room. Chatkit allows us to do this via the leaveRoom method. All we have to do is supply the ID of the room that we want to leave. Note that leaving a room doesn’t automatically cancel our existing subscriptions on that room. That’s why we need to cancel it separately. Otherwise, we’ll still continue to receive updates from the room that we just left, but we’ll actually get an error every time we do since we’re no longer a member of that room:

    backToUsers = () => {
      this.currentUser
        .leaveRoom({ roomId: this.roomId })
        .then(room => {
          this.currentUser.roomSubscriptions[this.roomId].cancel(); // cancel all the room subscriptions

          // reset the values
          this.roomId = null;
          this.chatWithUser = null;

          this.setState({
            currentScreen: "users",
            messages: [],
            currentRoomId: null,
            chatWithUser: null,
            inChatRoom: false
          });
        })
        .catch(err => {
          console.log(
            `something went wrong while trying to leave the room: ${err}`
          );
        });
    };

    // next: add updateMessage function

Next, updateMessage is executed when the user types something in the text field for entering their message. This is also a good place to call the isTypingIn function provided by Chatkit. This allows us to trigger the onUserStartedTyping hook for the user we’re chatting with:

    updateMessage = message => {
      this.setState({
        message
      });
      this.currentUser.isTypingIn({ roomId: this.state.currentRoomId });
    };

    // next: add sendMessage function

Next, the sendMessage function is executed when the user clicks on the send button in the Chat screen. This makes use of the sendMessage function provided by Chatkit. Once the message is sent, this triggers the onNewMessage hook on both the sender and receiver:

    sendMessage = () => {
      if (this.state.message) {
        this.currentUser
          .sendMessage({
            text: this.state.message,
            roomId: this.state.currentRoomId
          })
          .then(messageId => {
            this.setState({
              message: ""
            });
          })
          .catch(err => {
            console.log(`error adding message to room: ${err}`);
          });
      }
    };

    // next: add loadPreviousMessages function

Next, the loadPreviousMessages function is executed when the user does the “pull to refresh” gesture inside a chat room. There’s quite a bit happening here so we’ll break it down:

  1. Get the ID of the oldest message. Chatkit only returns the five most recent messages sent in a specific chat room because we’ve set it when we subscribed to the room (see subscribeToRoom function). The first time the user pulls to refresh, we’re getting the oldest of these five most recent messages.
  2. Set refreshing to true so the user sees a loading animation while the app fetches the old messages.
  3. Fetch the older messages in the current room. The most important thing to supply here is the initialId and the direction. The initialId is the ID of the message where we want Chatkit to start fetching. And direction is the order which we want to fetch. older means we want to fetch backward. So if the initialId is 45, it’s going to fetch messages with IDs from 40 to 44 if the limit is 5:
  4. Once the messages are returned, we push the message data to the array of old messages. We can’t just directly push it to the array containing the messages because we’re working with old data, so we concatenate the old messages and messages.

Here’s the code:

    loadPreviousMessages = () => {
      const oldestMessageId = Math.min(
        ...this.state.messages.map(m => parseInt(m.key))
      );

      this.setState({
        refreshing: true
      });

      this.currentUser
        .fetchMessages({
          roomId: this.state.currentRoomId,
          initialId: oldestMessageId,
          direction: "older",
          limit: 5
        })
        .then(messages => {
          let currentMessages = [...this.state.messages];
          let old_messages = [];

          messages.forEach(msg => {
            let isCurrentUser = this.currentUser.id == msg.sender.id ? true : false;

            old_messages.push({
              key: msg.id.toString(),
              username: msg.sender.name,
              msg: msg.text,
              datetime: msg.createdAt,
              isCurrentUser
            });
          });

          currentMessages = old_messages.concat(currentMessages);

          this.setState({
            refreshing: false,
            messages: currentMessages
          });
        })
        .catch(err => {
          console.log(`error loading previous messages: {$err}`);
        });
    };

    // next: add setScrollViewRef function

Next is the setScrollViewRef function, which sets the ScrollView ref to a local class variable. As you’ve seen earlier, this reference allows us to automatically scroll the ScrollView containing the messages to the bottom of the screen:

    setScrollViewRef = ref => {
      this.scrollViewRef = ref;
    };

    // next: add leavePresenceRoom function

Next, the leavePresenceRoom is executed when the user logs out of the app. Here, we use the leaveRoom function from Chatkit to leave the general room and cancel all subscriptions to it. We also clean up the state and the variable we used for tracking the current user:

    leavePresenceRoom = () => {
      this.currentUser
        .leaveRoom({ roomId: this.state.presenceRoomId })
        .then(room => {
          this.currentUser.roomSubscriptions[this.state.presenceRoomId].cancel();
          this.currentUser = null;
          this.setState({
            presenceRoomId: null,
            users: [],
            userHasLoggedIn: false,
            currentScreen: "login"
          });
        })
        .catch(err => {
          console.log(
            `error leaving presence room ${this.state.presenceRoomId}: ${err}`
          );
        });
    };

Running the app

At this point, the app should be fully functional, run it if you haven’t done so already. Make sure you’re inside the root of the project directory:

    expo start

If you’re logged into your Expo account in the device where you installed Expo as well as the Expo CLI, it should automatically detect the most recent project you’re working with so all you have to do is select it. If not, you can also use the QR scanner in the Expo client app to get the project.

If you encounter an issue that looks like the one below, this is because of the whatwg-fetch package that Expo depends on. It was recently updated to version 3.0.0, which broke Expo:

The solution is to install a lower version of the package by using Yarn. Execute the command below at the root of your project directory:

    yarn add whatwg-fetch@2.0.4

Where to go from here?

This tutorial was mainly geared towards beginners, this is the reason why the code was very simple, straightforward, and well, dirty-looking. Here are some tips for developing the app further:

  • Use Redux to maintain a general app state. In this tutorial, we had to pass a whole bunch of props to each of the screens and maintain them separately. By using Redux, we can remove a lot of boilerplate code and state updates all over the place.
  • Use React Navigation to add routing capabilities to the app. In this app, we simply used the state to selectively show which screen we want to show. By using React Navigation, we can declare all the screens of the app in a single file and navigate to each of them using very simple code.
  • Use the async-await pattern. In this app, we used a whole lot of promises and we often got really deep because of the conditions we had to check, and other requests we needed to make. By using the async-await pattern, we can avoid deeply-nested promise chains.
  • Use the Gifted Chat component by Farid Safi. This component can really simplify the code for your chat app a lot. The component’s author has also written a tutorial about it in the Pusher tutorial hub: Build a Chat App with React Native and Gifted Chat.
  • Add cursors and file attachment features to the app. These are the two other features that Chatkit allows you to implement.
  • Add a /token route to the server for generating tokens to be used for making requests to Chatkit. This allows us to use Chatkit in production.
  • Add a code to the server that will automatically kick a user out of the room if they’ve been idle for at least an hour. This should solve the issue that was mentioned earlier about users not leaving the room.

Conclusion

That’s it! In this tutorial, you learned how Chatkit really makes it easy to implement common features that you’ll find in chat applications.

You can view the source code of the app in this GitHub repo.

  • Chatkit

© 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.