CodeDevStack

Writing a WhatsApp Web Chrome extension

September 23, 2020

Last weekend I built a Chrome extension for WhatsApp Web which let’s you write little notes linked to your contacts.

It lead to some interesting research so I wanted to write this article to show how I did it.

The final extension looks like this:

Design is my passion

The extension injects some code into the DOM to create the UI and handles the notes creation and deletion. No editing for now.

The whole app is writen using React without any fancy external libraries, well, only dayjs.

The problems

  • How do I inject the code into the correct location to achieve an integrated look?
  • How do I detect which user am I talking to?
  • Is there a unique id I can use to link my notes to a specific user?

How do I inject the code into the correct location to achieve an integrated look?

Chrome extensions let you to select when you want to inject your code: At document start, idle or end. Perfect! I said. But of course, none of the default options solved this problem.

The problem comes from the fact that WhatsApp Web has an initial empty state which gets replaced once you click on a contact. Sure enough, the DOM element where we need to inject our React app doesn’t exist until you click on a contact.

I found a way to solve this using MutationObserver. This awesome DOM API lets me subscribe to DOM changes and do stuff every time it changes:

const observer = new MutationObserver(() => {
    const mainNode: Element | null = document.body.querySelector("#main");

    if (!mainNode?.parentNode) return;

    const root = mainNode.parentNode as Element;
    const app = document.createElement("div");
    app.id = "my-extension-root";

    if (!root.parentNode) return;

    root.parentNode.appendChild(app);

    ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
      app
    );
    observer.disconnect();
  });

  observer.observe(document.body, config);

All I needed to do was observe the DOM until my precious #main element showed up. After that, easy peasy, inject the React app and we are ready to go.

How do I detect which user am I talking to?

Ok, so I’m in. I was able to inject the app right where I wanted it. But detecting the user is a little bit more complicated. I can see the contact name right there, so I should be able to querySelector it, right? Well, WhatsApp Web uses mostly random class names and almost no ids.

Looking at the elements I was able to see the one with the contact name has a title attribute.

<span dir="auto" title="Tony Stark" class="_3ko75 _5h6Y_ _3Whw5">Tony Stark</span>

Interesting. A few other of the elements in the header section have title set, but the contact name always comes first. So in the end I was able to querySelector("[title]") the name. It might be a little bit fragile, but we are hooking into the app as best as we can.

Is there a unique id I can use to link my notes to a specific user?

Once I figured out how to get the name, unique ids were just a matter of figuing out where I could find them. The answer? The chat bubbles.

<div tabindex="0" class="_2hqOq message-in focusable-list-item" data-id="false_XXXXXXXXXXXX@c.us_FFFFFFFFFFF087C8"></div>

Success! The data-id contains a unique id for each user (replaced here with XXXXXXXXXXX). This id is the user’s phone number followed by @c.us.

Some querySelector magic…

const data = mainNode.querySelectorAll(
      "[data-id^='false_'],[data-id^='true_']"
    );

Followed by some regex to extract the id:

const userIdAttribute = data[0].getAttribute("data-id") || "";
const userIdMatch = userIdAttribute.match(/\d*@c.us/);

Yes! Now I have my contact name AND a unique id for linking my notes!

But, you might be wondering, how do I use this inside react? And even more importantly, how do I get updated data whenever I switch to another chat?

Again, MutationObserver comes to the rescue. I created a custom hook which subscribes to some DOM changes and updates a local state with the current user’s name and id.

Inside my react app all I need to do is call

const { user } = useUser();

which always returns the current user ready to be passed down as a prop or even use it wherever you need the current user.

And that’s it! Once I was able to have that piece of data injected into my React app, the rest was just normal extension building.