🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Implementing read cursors feature in a React Native chat app

  • Wern Ancheta

August 22nd, 2019
Basic knowledge of of React Native and Node will be useful. You will also need an ngrok account.

In this tutorial, we’ll take a look at how to implement the “seen” feature that we usually see in popular messaging apps like Facebook messenger or WhatsApp. We’ll use React Native to build the app and Chatkit for implementing the chat functionality.

Being able to see who has read your message in a chat room is a very useful feature.

Prerequisites

Basic knowledge of React Native, Node.js and Chatkit is required in order to follow this tutorial.

You also need a Chatkit account and a Chatkit app instance.

Lastly, you’ll need an ngrok account for exposing the server to the internet.

App overview

We will be updating an existing chat app to include the “seen” feature. When a member of the room scrolls through the messages, a read cursor will automatically be set. So that when another member long presses one of the messages, they’ll see the usernames of the users who has their cursor set on that message.

Here’s what it will look like:

You can get the full source code of this tutorial on this GitHub repo.

Setting up the app

So that we can focus on the main topic of this tutorial, I’ve created a starter branch in the GitHub repo that contains the basic chat app code. You can set it up by executing the following commands:

    git clone https://github.com/anchetaWern/RNChatkitReadCursors.git
    cd RNChatkitReadCursors
    git checkout starter
    yarn
    react-native link react-native-config
    react-native link react-native-gesture-handler

React Native Config, one of the dependencies requires you to update the android/app/build.gradle with their config if you’re on Android:

    apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" // 2nd line

At this point, you can also add your Chatkit credentials to the .env and server/.env files:

    // .env
    CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID"
    CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"
    CHATKIT_TOKEN_PROVIDER_ENDPOINT="YOUR CHATKIT TOKEN PROVIDER ENDPOINT"


    // server/.env
    CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID"
    CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"

Overview of the existing app

Before we proceed to actually updating the app, I’d like to first give you an overview of what features have already been coded, which modules were used, and the overall setup of the app.

First off, the following features are already available:

  • Logging in an existing user - you’ll have to create a user via the Chatkit app console so that you can login.
  • Joining a room - the user you created can join a room, provided its not set to private. When joining a room, the user is automatically entered once they’ve joined.
  • Entering a room - the user can enter a room they’re already a member of.
  • Sending and receiving messages
  • Loading previous message - users are able to view older messages that are not currently displayed in the screen.

Next, the following modules are used:

  • @pusher/chatkit-client - for implementing the underlying chat functionality.
  • react-native-gifted-chat - for easily building the chat UI.
  • axios - for communicating with the app’s server component.
  • react-navigation - for implementing the navigation between screens.

Lastly, the overall setup is that the app has both a frontend and backend components. Most of the backend is already handled by Chatkit, but we also need to use Chatkit’s server API in order to implement the following:

  • Checking if username used for logging in is valid.
  • Getting the user’s list of rooms and joinable rooms.
  • Joining a room.

Updating the app

Now that you know a bit about the app that we’re going to update, it’s time to dive into some code. First, we’ll add the code for getting the cursors for a specific room. This specific functionality is only available in Chatkit’s server APIs, that’s why we need to add this code to the server.

Getting the cursors

Add a new POST route called /read-cursors. A request is made to this route whenever a user long presses on a message to view who has seen it, This accepts the room_id and message_id as its request parameters. The room_id is the unique ID of the room and the message_id is the ID of the message that was long pressed by the user. To get the list of cursors in the room, we call Chatkit’s getReadCursorsForRoom() function. This accepts an object containing the roomId as its argument. This returns an array of objects with the position of the cursor. This is essentially the ID of the message that was seen by the user. We filter the results to only include the cursors with the same message ID supplied in the request:

    // server/index.js
    app.post("/read-cursors", async (req, res) => {
      const { room_id, message_id } = req.body;
      let seen_by = '';

      try {
        const cursors = await chatkit.getReadCursorsForRoom({
          roomId: room_id,
        });

        const read_by_members = cursors.filter(item => item.position == message_id);
        // next: get the usernames 

      } catch (err) {
        console.log('read cursor error: ', err);
      }
    });

Here’s a sample response that gets returned when calling getReadCursorsForRoom():

    <TODO: add json>

Next, we check if there are still any cursors left after filtering. If there is, then we call the getUsersById() function and supply it the userIds of the users who viewed the message. This allows us to get the usernames of those users. From there, all we have to do is extract the name from the results and convert it to a comma-separated value:

    if (read_by_members.length > 0) {
      const members = await chatkit.getUsersById({
        userIds: read_by_members.map(item => item.user_id),
      });

      seen_by = members.map(item => {
        return item.name;
      }).join(', ');
    }

    res.send({
      seen_by
    });

Updating the frontend

Now we’re ready to update the app itself. This where we add the code for setting the read cursors as well as retrieving them from the server so the users can see who viewed a specific message.

First, open the src/screens/Chat.js file and import the additional modules we need:

    import { View, Text, ActivityIndicator, StyleSheet } from 'react-native'; // add StyleSheet
    import { GiftedChat, Bubble } from 'react-native-gifted-chat'; // add Bubble
    import axios from 'axios'; // for making requests to the server component

Next, initialize the variable for storing the base URL for the server component:

    const CHAT_SERVER = "YOUR NGROK HTTPS URL";

Next, add the function for getting the last message based on the scroll percentage. This is how the setting of read cursors will mainly work. At any given time, the state maintains the messages currently being displayed on the screen. It’s essentially an array, so the function below accepts an array of messages and the percentage of that array you want to get. So for an array with eight items, supplying 50 as the second argument means that it will take the first item up to the fourth. But we’re also adding two to take into consideration the area where the user inputs their message:

    function get_r_percent_last(arr, percent) {
      const count = arr.length * (percent / 100);
      return arr.slice(0, Math.round(count) + 2).pop();
    }

Next, update the render() method to include the onLongPress, renderBubble, and listViewProps as props for the GiftedChat component:

  • onLongPress - the function to execute when the user long presses any of the messages.
  • renderBubble - a custom function for rendering a chat bubble.
  • listViewProps - where we can attach the onScroll event so that we can listen for the event where the user scrolls through the messages.
    render() {
      const { messages, is_loading, show_load_earlier } = this.state;
      return (
        <View style={{flex: 1}}>
          {
            is_loading && <ActivityIndicator size="small" color="#0000ff" />
          }
          <GiftedChat
            messages={messages}
            onSend={messages => this.onSend(messages)}
            user={{
              _id: this.user_id
            }}
            loadEarlier={show_load_earlier}
            onLoadEarlier={this.loadEarlierMessages}

            // add these:
            onLongPress={this.onLongPressMessage}
            renderBubble={this.renderBubble}

            listViewProps={{
              scrollEventThrottle: 1000,
              onScroll: async ({ nativeEvent }) => {  
                const percent = Math.round((nativeEvent.contentOffset.y / nativeEvent.contentSize.height) * 100); 
                const last_message = get_r_percent_last(this.state.messages, percent);

                await this.currentUser.setReadCursor({
                  roomId: this.room_id,
                  position: last_message._id
                });
              }
            }}
          />
        </View>
      );
    }

Before we proceed with the rest of the code, let’s break down the listViewProps first. Here, we’ve supplied an object containing the scrollEventThrottle which is the time interval (in milliseconds) which controls how often we fire the onScroll event while the user is scrolling. In this case, we’re only firing it every one second. This is where we calculate the percentage of the entire ScrollView in which the user is currently at. nativeEvent.contentOffset.y contains the current scroll position. Note that the max value of this one is dependent on the amount of content that is rendered in the scroll view. nativeEvent.contentSize.height is the height of the entire ScrollView. So dividing the two allows us to get the percentage scrolled. From there, we just get the last read message using the get_r_percent_last() function and set the read cursor using the ID of that message:

    listViewProps={{
      scrollEventThrottle: 1000,
      onScroll: async ({ nativeEvent }) => {  
        const percent = Math.round((nativeEvent.contentOffset.y / nativeEvent.contentSize.height) * 100); 
        const last_message = get_r_percent_last(this.state.messages, percent);

        await this.currentUser.setReadCursor({
          roomId: this.room_id,
          position: last_message._id
        });
      }
    }}

Note: Gifted Chat uses a FlatList for rendering the messages, that’s why I was referring to it as ScrollView earlier because FlatList is pretty much like the ScrollView component. Also note that the formula used in the above code isn’t perfect. It’s only an estimate of what the position of the current message is based on the percentage of the scroll position. This works for smaller-sized messages, but it will fail for more thought-out conversations that has multiple lines of text per chat bubble. Lastly, it’s important to note that the code above will only set one cursor per user in the room. This means that if the user scrolls downwards to see the previous messages, their cursor will actually be set in the older messages. This will often lead someone to assume that they haven’t read any of the newer messages especially if they don’t scroll back to the newer one’s.

Next, we add the renderBubble() function. This returns Gifted Chat’s Bubble component that is wrapped in a View. This allows us to add the text below the chat bubble which shows the usernames of the users who has seen the message. The current message is available via the bubbleProps argument that’s automatically passed by Gifted Chat to the function:

    renderBubble = (bubbleProps) => {
      const seen_by_users = bubbleProps.currentMessage.seen_by ? bubbleProps.currentMessage.seen_by : '';
      const view_style = seen_by_users ? { paddingBottom: 5 } : {};
      return (
          <View style={view_style}>
            <Bubble {...bubbleProps} />
            <Text style={styles.small_text}>{ seen_by_users }</Text>
          </View>
      );
    }

Next, add the onLongPressMessage() function. This gets called when the user long presses any of the messages. This is where we make a request to the server to get the read cursors for the current room. Then we update the messages to add the seen_by property to the message that was pressed:

    onLongPressMessage = async (context, message) => {
      try {
        const response = await axios.post(
          `${CHAT_SERVER}/read-cursors`, 
          { 
            room_id: this.room_id,
            message_id: message._id
          }
        );

        // add the seen_by property to the message that was pressed
        this.setState(state => {
          const messages = state.messages.map((item) => {
            if (item._id == message._id) {
              return { ...item, seen_by: response.data.seen_by };
            } else {
              return { ...item, seen_by: '' };
            }
          });

          return {
            messages
          }
        });
      } catch (err) {
        console.log('error getting cursor: ', err);
      }
    }

Lastly, add the styles:

    const styles = StyleSheet.create({
      small_text: {
        fontSize: 10,
        color: '#5d5d5d'
      }
    });

Running the app

At this point, you can now run the app.

First, run the server:

    node server/index.js
    ~/ngrok http 5000

Don’t forget to update the src/screens/Rooms.js and src/screens/Chat.js file with your ngrok HTTPS URL:

    const CHAT_SERVER = "YOUR NGROK HTTPS URL";

Then run the app:

    react-native run-android
    react-native run-ios

In order to test the app properly, you should create two or more users on your Chatkit app console and use it to login to two separate devices or emulator. Scrolling through the messages will automatically set the read cursor and any room member will be able to see who viewed a specific message by long-pressing on the message bubble.

Conclusion

In this tutorial, you learned how to implement the “seen” feature in a React Native chat app. As you’ve seen, we were only able to estimate the position of the message based on the current scroll position so it might not be perfect. So as a next step, I encourage you to come up with your own formula for estimating the viewed message.

You can get the full source code of this tutorial on this GitHub repo.

Clone the project repository
  • Chat
  • Node.js
  • JavaScript
  • React Native
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

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