5 min readUpdated Jan 25, 2024

Using Presence in in-game chat: Is the other person still there?

Using Presence in in-game chat: Is the other person still there?
Nate EagleNate Eagle

This is the third in a series of posts on adding an in-game chat room with React. 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:

import { useChannel, usePresence } from "ably/react";

Add opponentIsPresent to the GameContext type and object:

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.

 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:

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

Create an OpponentPresence component

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

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:

 import OpponentPresence from "./OpponentPresence";

Then add the component just before the close tag of the <ul> that displays the chat messages.

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

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.

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.

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 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, drop them a line in /r/ablyrealtime, or follow me @neagle and let us know how you’re getting on.

Join the Ably newsletter today

1000s of industry pioneers trust Ably for monthly insights on the realtime data economy.
Enter your email