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 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
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
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
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
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.
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.
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
;
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.
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.
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
.
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:
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);
}
};
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),
});
}
);
};
The createChatIfNotExist
function above does the following:
- Uses the Appwrite
getDocument()
method to get thegeneral-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";
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]);
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);
};
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>
Write in the input
message and go to the Documents tab on Appwrite's project dashboard to see the saved documents.
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();
};
}, []);
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
}
);
};
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>
Here is how our chat app looks.
Conclusion
This article discussed using Appwrite’s Realtime feature to subscribe to application events and display a typing indicator on a chat application.