SquadSync | Discord Clone, Part 2 - Autocomplete Emojis

If you'd like to follow along, or take a closer look at the complete code, you can check out the github repo here

This is a fun one! I've been busy with work-related stuff for the past week, so I wanted to ease myself back into this project.

No discord clone would be complete without emoji support, and I wanted to try and recreate the elegant autocomplete feature that discord has. If you're not familiar with it, it can be activated by typing a colon ':' followed by an emoji name in the discord textbox. This then pops up an emoji suggestion list, and hitting tab adds the selected emoji to your message.

What I've come up with isn't a complete or perfect implementation, but it's a good jumping off point. So let's get started.

The basic setup

We're going to need to handle the state of two things - our text input value, and our autocomplete value. So let's set up two variables for that, along with a couple HTML elements to display the data.

// the input state
const [msg, setMsg] = useState("");
// the autocomplete state
const [autoComplete, setAutoComplete] = useState("");


return(
    <>
        <div id="autoComplete">{autoComplete}</div>
        <textarea
            name="content"
            id="chatInput"
            value={msg}
            onChange={registerEmoji}
            onKeyDown={autocompleteEmoji}
            />
    </>
)

The autocomplete div is where we can display the emoji match, and the textarea will be the message box that we'll be manipulating.

You can see I've already set up a few event listeners. onChange will fire whenever we type into the textarea. This will be where we check for matches, and set our autoComplete value.

onKeyDown will listen for a 'tab' keypress, and can be used to autocomplete suggestions before typing out the full emoji name.

Of course, we'll also need a way to correlate these shortcode names to an actual emoji. That's where the emojis array comes in.

const emojis = [
    {
        name: ":smirk:",
        content: "😏",
    },
    {
        name: ":hot-face:",
        content: "🥵",
    },
    {
        name: ":sunglasses:",
        content: "😎",
    },
];

So just a basic array of objects, each with name and content.

The functionality

So here is how we'll find matches. OnChange will run the registerEmoji function whenever you type into the textarea. Inside this function, we can run a forEach loop on the emojis array.

Inside this loop, we first check if the message contains a colon ':'. If so, split the string, and take the 1st index, which will be contain the text after the colon. From there, run an includes function on the emoji.name value, passing the match string.

If the condition passes, we set our autocomplete state to the corresponding emoji content (the emoji itself) with a call to the setAutocomplete function. If it doesn't match, we do the same, but set an empty value.

This portion of the code will handle the autocomplete suggestion, but we also need to control what happens if the user enters the complete emoji name. In this case, we have a separate conditional that checks if the msgvariable contains the entirety of an emoji name. If so, it will replace the shortcode with the emoji.

  function registerEmoji(e) {
    var msg = e.target.value;
    //loop through our emojis array
    emojis.forEach((emoji) => {
        //check for colon
      if (msg.includes(":")) {
        //split the text value, check only the content after the colon
        var match = msg.split(":")[1];
        //set autocomplete based on results
        match = emoji.name.includes(match)
          ? setAutoComplete(emoji.content)
          : "";
      } else {
        //if no match, clear the autocomplete
        setAutoComplete("");
      }
      //if exact match, replace with emoji
      if (msg.includes(emoji.name)) {
        msg = msg.replace(emoji.name, emoji.content);
      }
    });

    setMsg(msg);
  }

Pretty cool! So how do we actually utilize the autocomplete value? Well, going back to the example discord gives us - when a user starts writing out an emoji shortcode, it provides suggestions based on that input. Then, the user can hit tab to select the suggested emoji, and populate their message with it.

This is where the onKeyDown listener comes into play. This function will fire when a user hits any key while focused on the textarea, including the tab key.

In this function, we will check if the autocomplete variable is populated. If so, we'll replace the partially entered shortcode with the matched emoji.


  function autocompleteEmoji(e) {
    let msg = e.target.value;
    //check that autocomplete is empty, and that the keypress was a tab
    if (autoComplete != "" && e.keyCode == 9) {
        //preventDefault allows us to maintain focus inside the textarea
      e.preventDefault();
      //split the message, and replace anything after the colon with the autoComplete value
      msg = msg.split(":");
      try {
        msg[1] = autoComplete;
        msg = msg.join("");
        setMsg(msg);
        //clear the autocomplete
        setAutoComplete("");
      } catch (er) {
        console.log("oofy", er);
      }
    }
  }

Caveats

The use of an includes function to check for matches isn't perfect. It will autopopulate with the first emoji in the array right after typing in ':'. It can also return inaccurate matches based on the first 1-2 characters you enter. This could be partially resolved by setting a minimum character count.

Another issue is how to handle it if a user goes back into the middle of a message to try an add an emoji. Currently the autocomplete function would remove the entirety of the message after the shortcode begins. This might be partially resolved by checking for a space ' ' or some other character to limit the amount of content that gets replaced.

Conclusion

So that's that! Pretty fun feature if you ask me. Like I said above, there is some cleaning up to do. It would also be cool to return an entire array of potential matches, the same way discord does. But for now, I'm pretty pleased with this.