Using Presence in in-game chat: Is the other person still there?
Tags: webdev, react, reactjsdevelopment
Published: 2024-05-01 08:27:38

This is the third in a series of posts on [adding an in-game chat room with React](https://hubs.la/Q02jNDfC0). In the first post, we added a chat room to a game of tic-tac-toe. In the second, we used Presence to add typing indicators that show us when the other person in the chat is typing.

With all of that in place, this post will build on our chat app even further. Here, we’ll use Presence again but this time to show us if the other person leaves the game – so that we don't stick around in an abandoned game or say "gg" to someone who's already gone.

## Managing Presence

In a standard chat app, you might just want to display a list of everyone in the chat, but in our instance we can customize the behavior a bit more. We can boil what we're interested in down to a boolean called `opponentIsPresent`, and display a system message in the chat whenever the opponent leaves. Even better: we can give the user a link to start a new game so that if their opponent leaves halfway through, they're not stuck.

Let's augment the `GameContext` in the `Game` component to contain the `opponentIsPresent` boolean, then create a new component called `OpponentPresence` that we can add to `Chat`, so that we keep all of the new logic nicely self-contained.

### Add to Game

In `src/app/components/Game.tsx`, let's do the following:

Add `usePresence` to the `import` for Ably's React hooks:

```javascript
import { useChannel, usePresence } from "ably/react";
```
Add `opponentIsPresent` to the `GameContext` type and object:

```javascript
type GameContext = {
  opponentId: string;
  opponentIsPresent: boolean;
};

const GameContext = createContext<GameContext>({
  // Set defaults
  opponentId: "",
  opponentIsPresent: false,
});
```
Then, below where we set the `opponentId`, add this code to get `presenceData` from Ably's `usePresence` hook and then check it to try to find the `opponentId`. Note that we're using `useMemo` to avoid having this calculation run unnecessarily: we only need it to run when `presenceData` or `opponentId` change.

```javascript
 const { presenceData } = usePresence(`game:${game.id}`);
  const opponentIsPresent = useMemo(
    () => Boolean(presenceData.find((p) => p.clientId === opponentId)),
    [presenceData, opponentId]
  );
```
Last, we just need to add `opponentIsPresent` to the `GameContext.Provider` value:

```javascript
 <GameContext.Provider value={{ opponentId, opponentIsPresent }}>
```

### Create an OpponentPresence component

Create `src/app/components/chat/OpponentPresence.tsx` file, and copy in this code:

```javascript
import { useEffect, useState } from "react";
import { useAppContext } from "../../app";
import { useGameContext } from "../Game";

// This component shows a message if the opponent has left the game.
// It also provides an important link to restart the game if the opponent has
// abandoned an in-progress game.

const OpponentStatus = () => {
  const { gameResult, fetchGame, setGame } = useAppContext();
  const { opponentIsPresent } = useGameContext();

  const [shouldShowOpponentMessage, setShouldShowOpponentMessage] =
    useState(false);

  // Only show the opponent's status after a delay. This prevents an initial
  // flicker in the chat window as the game gets underway.
  useEffect(() => {
    let timer: NodeJS.Timeout | null = null;

    if (!opponentIsPresent) {
      timer = setTimeout(() => {
        setShouldShowOpponentMessage(true);
      }, 1000);
    } else {
      if (timer) {
        clearTimeout(timer);
      }
      setShouldShowOpponentMessage(false);
    }

    return () => {
      if (timer) {
        clearTimeout(timer);
      }
    };
  }, [opponentIsPresent]);

  return (
    <>
      {shouldShowOpponentMessage && !opponentIsPresent ? (
        <li className="system-message">Your opponent has left the&nbsp;game.</li>
      ) : null}
      {shouldShowOpponentMessage && !opponentIsPresent && !gameResult ? (
        <li className="play-again-link">
          <a
            onClick={() => {
              fetchGame(true)
                .then(setGame)
                .catch((error) => console.log(error));
            }}
          >
            Play again?
          </a>
        </li>
      ) : null}
    </>
  );
};

export default OpponentStatus;
```

The most important part to note is where it gets `opponentIsPresent` from `GameContext`. It has a small amount of fanciness involved: it has debounce logic to prevent brief flickers when the game first begins or when an opponent refreshes the page. (When responding to user actions, debouncing can often smooth out experiences that could otherwise be jarring or annoying.)

It also has some logic to make it possible to restart the game if one's opponent has left.

Now, we need to pull in the new component to `Chat`. In `src/app/components/chat/Chat.tsx`, import the component:

```javascript
import OpponentPresence from "./OpponentPresence";
```
Then add the component just before the close tag of the `<ul>` that displays the chat messages.

```javascript
     <ul className="p-2 grow bg-white min-h-[200px] sm:min-h-0">
        {messages.map((message) => {
          ...
        })}
        <OpponentPresence />
      </ul>
```

### Try it out!

Start a game in two different browsers, then close one of them. After a second, you should see a message show up in the chat screen of the remaining player, giving them the option of starting a new game. And the great thing is, if you open the game back up in your other browser, the message will disappear - showing that the other player is back.

![Tic-tac-toe-presence](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d8nehzy5tgxz8cf7v78w.png)

### A note for debugging

Ably's dashboard can be a huge help in debugging channels and presence while you're developing an application. If you log in to Ably and go to your dashboard then select your app, you can click on "Dev console." A little way down the page, there's an area to attach to a specific channel.

![Tic Tac Toe Debugging](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u5e0mk02dxp0ieozduwr.png)

Add a console.log to your Chat component to find out your game ID, then add `game:<game.id>` to the box in the top right and then attach to the channel.

![Debugging with Ably](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6ihn3lz9bka13oy53i8.png)

You'll have an interface where you can publish custom messages, see all messages coming through, and see--on the right--all members listed as present. It's a great way to sanity check your channel's behavior if you hit any bumps during your implementation.

(Any issues? Compare what you have against the [4-presence](https://github.com/neagle/tic-tac-toe/tree/4-presence) branch.)

## Wrapping up

And that’s it, until next time! We now have functional presence and typing indicators inside a working chat application that sits alongside the tic-tac-toe game.

In the next post, I’ll take you through how to enable emoji reactions to your opponents messages.

Until then, you can tweet Ably [@ablyrealtime](https://twitter.com/ablyrealtime), drop them a line in [/r/ablyrealtime](https://www.reddit.com/r/ablyrealtime/), or follow me [@neagle](https://twitter.com/neagle) and let us know how you’re getting on.