DEV Community

Odewole Babatunde Samson for Hackmamba

Posted on

Build a real-time text-typing indicator in Next.js

A popular feature of chat applications is a real-time text-typing indicator, which displays the name or username of those currently typing.

What we will be building

This article discusses building a text-typing indicator in a chat application using the Appwrite Realtime service with Next.js. We will use Appwrite’s robust database and Realtime service to manage our application, subscribe to channels in our database, and display a text-typing indicator when changes occur in the channels.

GitHub URL

https://github.com/Tundesamson26/chat-app
Enter fullscreen mode Exit fullscreen mode

Prerequisites

  • Knowledge of JavaScript and React.js.
  • Docker Desktop installation on your local machine. Check the Get Docker documentation for guidance and verify its installation with docker -v.
  • Appwrite instance running on our computer; check out this article to create a local Appwrite instance.
  • Understanding Next.js is advantageous but is not compulsory. ## Setting up the Next.js app

Next.js is an open-source React framework that lets us build server-side rendered static web applications. To create our Next.js app, navigate to the preferred directory and run the terminal command below:

npx create-next-app
# or
yarn create next-app
Enter fullscreen mode Exit fullscreen mode

After creating the app, change the directory to our project and start a local development server with:

cd <name of our project>
npm run dev
Enter fullscreen mode Exit fullscreen mode

To see our app, we then go to http://localhost:3000.

Installing dependencies

Installing unique-username-generator

This package helps generate a unique username from randomly selected nouns and adjectives. To install unique-username-generator in our project, we run these terminal commands.

npm install unique-username-generator --save
Enter fullscreen mode Exit fullscreen mode

Installing Appwrite

Appwrite is an open-source, end-to-end, backend server solution that allows developers to build applications faster. To use it in our Next.js application, install the Appwrite client-side SDK by running this terminal command.

 npm install appwrite 
Enter fullscreen mode Exit fullscreen mode

Creating a new Appwrite project

During the creation of the Appwrite instance, we specified what hostname and port we use to view our console. The default value is localhost:80: navigate there and create a new account to see the console. On the console, click the Create Project button to start a new project.

Create project tab

Our project dashboard appears once we have created the project. At the top of the page, click the Settings bar to access our Project ID and API Endpoint.

Setting console

Next, we'll copy our Project ID and API Endpoint, which we need to initialize our Web SDK code. In the root directory of our project, we create a utils folder, which will hold our web-init.js file. This file configures Appwrite in our application.

In the utils/web-init.js file, we initialize our Web SDK with:

// Init your Web SDK
import { Appwrite } from "appwrite";

export const sdk = new Appwrite();
    sdk
      .setEndpoint('http://localhost/v1') // Your Appwrite Endpoint
      .setProject('455x34dfkj') // Your project ID
;
Enter fullscreen mode Exit fullscreen mode

Creating a collection and attributes

On the left side of our dashboard, select the Database menu. Then, create a collection in the database tab by clicking on the Add Collection button. This action redirects us to a Permissions page.

At the Collection Level, we want to assign a Read Access and Write Access with a role:all value. We can modify the permissions to specify who has access to read or write to our database.

Collection level

On the right side of our "Permissions" page, copy the collection ID, which we need to perform operations on documents in this collection.

Next, go to the attributes tab to create the fields we want a document to have. The properties in our case are is_typing, an array of usernames of the actively typing users.

Attribute console

Setting up the chat application web page

Our chat application will have a page: a mobile-sized chat app with a top menu, message, and input where the user will type. This page will also subscribe to the typing event and display its updates in real time. Create this chat application with the GitHub gist below.

From the gist below, we have the pages/index.js.

https://gist.github.com/Tundesamson26/d63a0e073caba6e3ee552cd336d67042

https://gist.github.com/Tundesamson26/d63a0e073caba6e3ee552cd336d67042

In the index.js, we did the following:

  • Imported required dependencies and components.
  • Implemented state variables to store the messages. This contains a list of all messages sent and received, username, and typers; this is the array holding the user typing in the document.
  • Top Menu: This contains the application title and a section to show who is currently typing.
  • Input: This contains the text field to input messages and the send button.

At this point, our application should look like so:

Chat-application-UI

Creating an anonymous user session

Appwrite requires a user to sign in before reading or writing to a database to enable safety in our application. However, we can create an anonymous session that we'll use in this project. We'll do so in our web-init.js file.

// Init your Web SDK
import { Appwrite } from "appwrite";

  export const sdk = new Appwrite();
    sdk
      .setEndpoint("http://localhost/v1") // Your API Endpoint
      .setProject("chatID"); // Your project ID
  export const createAnonymousSession = async () => {
      try {
        await sdk.account.createAnonymousSession();
      } catch (err) {
        console.log(err);
      }
};
Enter fullscreen mode Exit fullscreen mode

Creating database documents
We need to create a chat document that stores our list of typing users in the is_typing attribute. In the index.js file, write a createChatIfNotExist() function to create the document if it does not exist. For simplicity, we'll keep the id as general chat.

const createChatIfNotExist = () => {
        let promise = sdk.database.getDocument([COLLECTION_ID], "general-chat");
        promise.then(
          function (response) {
            setTypers(JSON.parse(response.is_typing));
          },
          function (error) {
            sdk.database.createDocument([COLLECTION_ID], "general-chat", {
              is_typing: JSON.stringify(typers),
            });
          }
        );
};
Enter fullscreen mode Exit fullscreen mode

The createChatIfNotExist function above does the following:

  • Uses the Appwrite getDocument() method to get the general-chat document ID.
  • The createDocument() method creates a document using the collection ID and data fields to be stored. This collection ID is the same ID we copied from our Permissions Page earlier.

This would post a ‘user is typing’ event for each input in the message text field. Though this is good, it is not optimal because every input will make a call to the Appwrite database.

Generating random username
Next, we need to generate a random username for each user typing the message input using our installed unique-username-generator package. First, import the dependency into the pages/index.js file.

import { generateUsername } from "unique-username-generator";
Enter fullscreen mode Exit fullscreen mode

Then write a conditional statement check for the current “user typing” on the mount of our application using the React useEffect() Hooks.

useEffect(() => {
    if (!username) {
      const _username = localStorage.getItem("username") || generateUsername();
      localStorage.setItem("username", _username);
      setUsername(_username);
    }
}, [username]);
Enter fullscreen mode Exit fullscreen mode

The code snippet above checks if the username does not exist, and it should generate a username and store the username in localStorage.

Setting timeout for updating our document
A better way to enable the ‘user is typing’ event for each input in the message text field is to set the time interval for updating our database.

We write a writeMessage() function in the index.js file to update our code to ensure we post only typing events to the appwrite once every 0.2 seconds.

const writeMessage = (e) => {
    clearTimeout(typing_timeout);
    typing_timeout = setTimeout(() => {
      if (typers.includes(username)) return;
      let promise = sdk.database.updateDocument(
        "chatCollection",
        "general-chat",
        {
          is_typing: JSON.stringify([...typers, username]),
        }
      );
      promise.then(
        function (response) {
          console.log(response); // Success
        },
        function (error) {
          console.log(error); // Failure
        }
      );
    }, 200);
};
Enter fullscreen mode Exit fullscreen mode

Next, we pass our writeMessage() function into an onKeyPress event listener on our input element in the pages/index.js.

<div className="message_input_wrapper">
  <input
    id="message-text-field"
    className="message_input"
    placeholder="Type your message here..."
    value={message}
    onChange={(e) => setMessage(e.target.value)}
    onKeyPress={writeMessage}
  />
</div>
Enter fullscreen mode Exit fullscreen mode

Write in the input message and go to the Documents tab on Appwrite's project dashboard to see the saved documents.

Document console

How the typing indicator will work

Before we proceed to the implementation, let’s explain how the text-typing indicator functionality works.

Subscribing to updates on the document
When the user starts typing in the message text field, the page sends a Realtime request to listen to any events on the server-side. This is broadcast to everyone as an event in Realtime using the subscribe method.

useEffect(() => {
    const _subscribe = sdk.subscribe(
      "collections.[COLLECTION_ID].documents",
      (response) => {
        const { payload } = response;
        if (payload?.$id === "general-chat") {
          setTypers(JSON.parse(payload.is_typing));
        }
      }
    );
    return () => {
      _subscribe();
    };
  }, []);
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we did the following:

  • Subscribe to a channel using Appwrite's subscribe method, which receives two parameters — the channel we subscribe to and a callback function. To learn more about the various channels we can subscribe to, check out Appwrite's Realtime Channels.

Next is to make our "user is typing" disappear when they click outside the message input. To achieve this, we write the handleBlur() function.

const handleBlur = () => {
    let promise = sdk.database.updateDocument(
      [COLLECTION_ID],
      "general-chat",
      {
        is_typing: JSON.stringify(typers.filter((e) => e !== username)),
      }
    );
    promise.then(
      function (response) {
        console.log(response); // Success
      },
      function (error) {
        console.log(error); // Failure
      }
    );
 };
Enter fullscreen mode Exit fullscreen mode

Next, we render our handleBlur() function into an onBlur event listener in our input element in the index.js file.

<div className="message_input_wrapper">
  <input
     id="message-text-field"
     className="message_input"
     placeholder="Type your message here..."
     value={message}
     onChange={(e) => setMessage(e.target.value)}
     onKeyPress={writeMessage}
     onBlur={handleBlur}
  />
</div>
Enter fullscreen mode Exit fullscreen mode

Here is how our chat app looks.

user-typing chat-application

Conclusion

This article discussed using Appwrite’s Realtime feature to subscribe to application events and display a typing indicator on a chat application.

Resources

Discussion (0)