Build a real-time text-typing indicator in Next.js
Tags: nextjs, javascript, react, tutorial
Published: 2022-06-21 15:22:45

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
```

## Prerequisites
- Knowledge of JavaScript and React.js.
- Docker Desktop installation on your local machine. Check the [Get Docker documentation](https://docs.docker.com/get-docker/) for guidance and verify its installation with `docker -v`.
- Appwrite instance running on our computer; check out [this article](https://dev.to/hackmamba/create-a-local-appwrite-instance-in-3-steps-19n9?utm_source=hackmamba&utm_medium=hackmamba) to create a local Appwrite instance.
- Understanding Next.js is advantageous but is not compulsory.

## Setting up the Next.js app
[Next.js](https://nextjs.org/) 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: 

```javascript
npx create-next-app
# or
yarn create next-app
```

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

```javascript
cd <name of our project>
npm run dev
```

To see our app, we then go to [http://localhost:3000](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.

```javascript
npm install unique-username-generator --save
```

**Installing Appwrite**

[Appwrite](https://appwrite.io/?utm_source=hackmamba&utm_medium=hackmamba-blog) 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.

```javascript
 npm install appwrite 
```

## 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](http://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](https://paper-attachments.dropbox.com/s_C222CF637E4B0001231B121EFEAD7AA1169030C2C822DCB84AC1C01B10D9C496_1650895817469_image.png)


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](https://paper-attachments.dropbox.com/s_475A30952A0689F542E95C26783556C8B550B57D322B980F805242869836FB42_1653512612996_image.png)


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:

```javascript
// 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
;
```

## 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](https://paper-attachments.dropbox.com/s_C222CF637E4B0001231B121EFEAD7AA1169030C2C822DCB84AC1C01B10D9C496_1650646239734_image.png)


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](https://paper-attachments.dropbox.com/s_475A30952A0689F542E95C26783556C8B550B57D322B980F805242869836FB42_1653905209524_image.png)



## 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](https://gist.github.com/Tundesamson26/ccbf77369ce4c75433eb4d38a4d31501) below.

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

{% embed 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](https://paper-attachments.dropbox.com/s_475A30952A0689F542E95C26783556C8B550B57D322B980F805242869836FB42_1653514209175_image.png)


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

```javascript
// 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);
      }
};
```

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

```javascript
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),
            });
          }
        );
};
```

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.
```javascript
import { generateUsername } from "unique-username-generator";
```

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

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

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.

```javascript
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);
};
```

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

```javascript
<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>
```

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

![Document console](https://paper-attachments.dropbox.com/s_475A30952A0689F542E95C26783556C8B550B57D322B980F805242869836FB42_1653947298725_image.png)

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

```javascript
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();
    };
  }, []);
```

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](https://appwrite.io/docs/realtime#channels?utm_source=hackmamba&utm_medium=hackmamba-blog) [Realtime](https://appwrite.io/docs/realtime#channels?utm_source=hackmamba&utm_medium=hackmamba-blog) [Channels](https://appwrite.io/docs/realtime#channels?utm_source=hackmamba&utm_medium=hackmamba-blog).

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

```javascript
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
      }
    );
 };
```

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

```javascript
<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>
```

Here is how our chat app looks.

![user-typing chat-application](https://videoapi-muybridge.vimeocdn.com/animated-thumbnails/image/b6803e4d-e726-4e12-8833-c527b2be3cac.gif?ClientID=vimeo-core-prod&Date=1653902987&Signature=795467dde2072265a54328994cd0cf1ad3785e99)



## Conclusion

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

## Resources
- [Getting started with Appwrite for web](https://appwrite.io/docs/getting-started-for-web?utm_source=hackmamba&utm_medium=hackmamba-blog)
- [Unique-username-generator](https://www.npmjs.com/package/unique-username-generator?utm_source=hackmamba&utm_medium=hackmamba-blog)
- [What Appwriteis and how to use it efficiently](https://appwrite.io/docs/getting-started-for-web?utm_source=hackmamba&utm_medium=hackmamba-blog)