6 min readUpdated Jan 30, 2024

How to enable reaction emojis for in-game chat with React

How to enable reaction emojis for in-game chat with React
Nate EagleNate Eagle

This is the last in a series of posts on adding an in-game chat room with React. In the first, we added a chat room to a game of tic-tac-toe. In the second, we used Presence to add a typing indicator. And in the third we used Presence to show whether or not an opponent has left the game.

Now, let's look at how to add the last feature - reaction emojis on our opponent's messages.

Enabling emoji reactions

When I was a kid, we had to remember complicated emoticons to express non-textual reactions to people we were chatting with. (8-O, >;‑), or even the immortal (╯° °)╯︵ ┻━┻!). But now many chat apps have a treasure trove of emojis available as reactions. Let's bring our chat up to date!

To do this, we're going to use Ably's message interactions. Message interactions "enable you to interact with previously sent messages," and they make possible a whole bevy of other features like threading, read receipts, and more. The most important thing they do is add a timeSerial to every message in a channel which is just a unique id that can be used as a reference for other interactions.

Armed with that timeSerial, we can publish add and remove emoji messages to the same channel we've been using.

The approach we will use here is to augment our Chat component with a second useState hook that will be a sibling to messages, called reactions. It will be an object, and it will look like this:

{
    <timeserial>: [
        { /* Reaction Messages */ },
        { /* Reaction Messages */ },
        { /* Reaction Messages */ }
    ],
    ...
}

In other words, every key will be a unique id (timeserial) that we can look up for any given message. When we receive add or remove reaction messages, we can add to or filter from the array.

Then, when we display our messages in Chat, we can add a bit of extra logic for each message to look up any reactions that might be there.

To do this, here's what we'll need:

  • Some new state logic in Chat , including new actions to handle add and remove reaction messages from the channel
  • A new component to display any reaction emojis in individual messages: DisplayReactions
  • A new component to let a user add or remove a reaction emoji for any of their opponent's messages: CreateReaction.tsx

New state logic in Chat

In src/app/components/chat/Chat.tsx, first add another useState hook:

const [messages, setMessages] = useState<ChatTypes.Message[]>([]);
const [reactions, setReactions] = useState<
  Record<string, Ably.Types.Message[]>
>({});

Then, we need to update the onMessage function that gets fired every time a new message comes in:

const onMessage = (message: Ably.Types.Message) => {
    const { name, clientId, data, timestamp, id } = message;
    if (name === "message") {
      setMessages((messages) => [
        ...messages,
        { clientId, text: data, timestamp, id },
      ]);
    }

    if (name === "add-reaction") {
      setReactions((reactions) => {
        const newReactions = { ...reactions };
        const key = data.extras.reference.timeserial;

        // Create a new array if it doesn't exist
        if (!newReactions[key]) {
          newReactions[key] = [];
        }

        // Prevent duplicates
        if (!newReactions[key].find((reaction) => reaction.id === id)) {
          newReactions[key].push(message);
        }

        return newReactions;
      });
    }

    if (name === "remove-reaction") {
      setReactions((reactions) => {
        const newReactions = { ...reactions };
        const key = data.extras.reference.timeserial;
        newReactions[key] = newReactions[key]?.filter(
          (reaction) => reaction.data.body !== data.body
        );
        return newReactions;
      });
    }
  };

Note that we're now listening for add-reaction and remove-reaction message name possibilities in incoming messages. Those are just my conventions: you can name them whatever you want. They just need to mirror the name you use for each when you publish reaction events.

Create a DisplayReactions component

// src/app/components/chat/DisplayReactions.tsx
import * as Ably from "ably";

type DisplayReactionsProps = {
  reactions: Ably.Types.Message[];
};

const DisplayReactions = ({ reactions }: DisplayReactionsProps) => {
  return (
    <div className="display-reactions">
      {reactions.map((reaction) => (
        <b key={reaction.id}>
          {reaction.data.body}
        </b>
      ))}
    </div>
  );
};

export default DisplayReactions;

Nothing fancy in this one.

Create a CreateReaction component

There's a fair amount of display logic, but the important thing to notice is just the use of two channel.publish calls, for add-reaction and remove-reaction, which match the message name possibilities we're listening for in Chat.

// src/app/components/chat/CreateReaction.tsx
import * as Ably from "ably";
import * as ChatTypes from "../../../types/Chat";
import classnames from "classnames";
import { useAppContext } from "../../app";

type ReactionProps = {
  channel: Ably.Types.RealtimeChannelPromise;
  message: ChatTypes.Message;
  reactions: Ably.Types.Message[];
  className?: string;
};

// This can be any list of possible emojis your heart desires
const EMOJIS = ["🤘", "🤪", "😡", "😂", "😭"];

const Reaction = ({
  channel,
  message,
  reactions,
  className = "",
}: ReactionProps) => {
  const { playerId } = useAppContext();

  const handleReactionClick = async (
    event: React.MouseEvent<HTMLAnchorElement>
  ) => {
    if (event.target instanceof HTMLAnchorElement) {
      const emoji = event.target.textContent;
      if (!emoji) return;
      if (alreadyReacted(emoji)) {
        await channel.publish("remove-reaction", {
          body: emoji,
          extras: {
            reference: { type: "com.ably.reaction", timeserial: message.id },
          },
        });
      } else {
        await channel.publish("add-reaction", {
          body: emoji,
          extras: {
            reference: { type: "com.ably.reaction", timeserial: message.id },
          },
        });
      }
    }
  };

  const alreadyReacted = (emoji: string) => {
    return reactions?.length
      ? reactions
          .filter((reaction) => reaction.clientId === playerId)
          .find((reaction) => reaction.data.body === emoji)
      : false;
  };

  return (
    <span className={`create-reaction ${className}`}>
      <button>
        <span>🙂</span>
        <ul>
          {EMOJIS.map((emoji) => (
            <li key={emoji}>
              <a
                onClick={handleReactionClick}
                className={classnames({
                  selected: alreadyReacted(emoji),
                })}
              >
                {emoji}
              </a>
            </li>
          ))}
        </ul>
      </button>
    </span>
  );
};

export default Reaction;

Add the new components to Chat

Import the new components into src/app/components/chat/Chat.tsx:

import CreateReaction from "./CreateReaction";
import DisplayReactions from "./DisplayReactions";

Then, add them into the <li> where we display individual messages:

{messages.map((message) => {
  const name = playerName(message.clientId, game.players);
  const isSystemMessage = !Boolean(name);

  if (isSystemMessage) {
    return (
      <li key={message.id} className="system-message">
        {message.text}
      </li>
    );
  } else {
    return (
      <li key={message.id} className="user-message">
        <b
          className={classnames(
            {
              "player-x": name === playerNames[0],
              "player-y": name === playerNames[1],
            },
            ["font-mono"]
          )}
>
          {name}:
        </b>{" "}
        {message.text}
        {message.clientId !== playerId && (
          <CreateReaction
            channel={channel}
            message={message}
            reactions={reactions[message.id]}
          />
        )}
        {reactions[message.id] && (
          <DisplayReactions reactions={reactions[message.id]} />
        )}
      </li>
    );
  }
})}

Try it out!

If you send yourself some chat messages, you should now be able to hover over any of your opponent's messages and get the ability to add and remove reaction emojis.

(Any issues? Compare what you have against the 5-reaction-emojis branch.)

Conclusion

With that, dear reader, I will leave you! I hope this series has shown that once you have the Ably client set up in your React app, you get to move very quickly from dealing with network communication to building features, which is where all the fun is. More than that: it's where you create your application's unique value, the things that make it different from what's already out there.

I hope this has given you a lot to work with, and a good starting point for adding chat to games or anywhere they might make sense in your React application. The entire source code of the application we built in this guide is available on Github.

We would love to hear about what you build and how you use React notifications! Tweet Ably @ablyrealtime or drop us a line in /r/ablyrealtime, or follow me @neagle.

Join the Ably newsletter today

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