Codelab: Build a Chrome Extension in JavaScript using Gemini

1. Introduction

Are you joining a meet call but don't want to be the first one to do so? If this describes you, we have the solution for you!

Following this codelab, you'll create a Chrome extension that alerts you when the very first participant joins the call.

You'll learn the different elements of a chrome extension and then deep dive into each part of the extension. You'll learn about extension functions such as content scripts, service workers and message passing.

You need to adhere to the manifest v3 release to be able to receive a notification whenever a participant joins a meet call.

2. Before you begin

Prerequisites

While this codelab is suitable for beginners, possessing a foundational understanding of Javascript can greatly enhance your experience.

Setup/Requirements

  • The Chrome browser
  • An IDE/Editor setup on your local system.
  • Install the gcloud cli if you wish to enable the Gemini API using gcloud.

Enable the Gemini API

Note that if you're writing the code in the Cloud Shell editor,
then you will have to download the folder somewhere on your local filesystem to test the extension locally.

3. Let the fun begin

Basic Extension Installation

Let's create a directory which we will be using as the root of our project.

mkdir gemini-chrome-ext
cd gemini-chrome-ext

Before we start asking specific questions to Gemini, let's ask some questions about the generic structure of a chrome extension.

Prompt:

What are the important parts to build a chrome extension?

We get a response specifying minor details about the manifest file, the background script and details about the user interface. Let's get into more detail for these specific files.

Prompt:

Create a manifest.json file to build a chrome extension.
Make the name of the extension "Meet Joinees Notifier"
and the author "<YOUR_EMAIL>"

You can use a desired name and your email in the author field.

Gemini returns the contents of the manifest file that we need, but we do get some extra fields which we won't need, like the action field. Also, we need a description. Let's fix that.

Prompt:

Remove the "action" field and make the description as
"Adds the ability to receive a notification when a participant joins a Google meet".

Let's put this content into the manifest.json file in the root of your project.

At this stage the manifest file should look something like this.

{
    "name": "Meet Joinees Notifier",
    "version": "1.0",
    "manifest_version": 3,
    "description": "Adds the ability to receive a notification when a participant joins a Google Meet",
    "author": "<YOUR_EMAIL>"
}

Remove any other additional fields generated in your manifest file for now as this codelab assumes these fields in your manifest file.

Now, how do we test that the extension works? Let's ask our friend Gemini, shall we?

Prompt:

Guide me on the steps needed to test a chrome extension on my local filesystem.

It does give me some steps on how to test it. Let's navigate to the "Extensions Page" by navigating to chrome://extensions and make sure to enable the "Developer Mode" button, which should reveal the "Load unpacked" button, which we can use to navigate to the folder that contains the extension files locally. Once we do that, we should be able to see the extension in the "Extensions Page".

3d802a497ce0cfc2.png

92db1999a1800ecd.png

Great! We can see our extension, but let's start to add some functionality.

4. Add a content script

We want to run some javascript code only on https://meet.google.com which we can do using content scripts. Let's ask Gemini, how to achieve that in our extension.

Prompt:

How to add a content script in our chrome extension?

Or more specifically:

Prompt:

How to add a content script to run on meet.google.com subdomain in our chrome extension?

Or another version:

Prompt:

Help me add a content script named content.js to run on meet.google.com subdomain
in our chrome extension. The content
script should simply log "Hello Gemini" when we navigate to "meet.google.com".

Gemini gives us exact changes we need to do in our manifest.json file and also the javascript we need in our content.js file.

With the addition of content_scripts, our manifest file becomes:

{
    "name": "Meet Joinees Notifier",
    "version": "1.0",
    "manifest_version": 3,
    "description": "Adds the ability to receive a notification when a participant joins a Google Meet",
    "author": "abc@example.com",
    "content_scripts": [
        {
          "matches": ["https://meet.google.com/*"],
          "js": ["content.js"]
        }
    ]
}

This tells chrome to inject the content script content.js whenever we navigate to a page in the subdomain " https://meet.google.com". Let's add this file and test it shall we?

Let's add this code in the content.js file.

console.log("Hello Gemini");

Sure enough! When we visit meet.google.com we see "Hello Gemini" on the javascript console(Mac: Cmd + Opt + J / Win/Linux: Ctrl + Shift + J).

manifest.json

{

"name": "Meet Joinees Notifier",

"version": "1.0",

"manifest_version": 3,

"description": "Adds the ability to receive a notification when a participant joins a Google Meet",

"author": "luke@cloudadvocacyorg.joonix.net",

"permissions": [

    "tabs",

    "notifications"

],

"content_scripts": [

    {

        "matches": [

            "https://meet.google.com/*"

        ],

        "js": [

            "content.js"

        ]

    }

]

}

content.js

console.log("Hello Gemini!");

6216bab627c31e6c.png

d61631cd9962ffe5.png

Great! Now we are in a position where we can add some javascript specific functionality to our app. Let's take a moment and think about what we are trying to achieve.

Improving the content script

We want to be able to get a notification when someone joins a meeting when we are on the meeting page(Where we have an option to join the meeting). In order to achieve this, let's observe how the screen changes visually when the meeting is empty vs when someone has joined the meeting.

This is how it looks when there is no one in the meeting.

fe5a0c95b20e7f72.png

Whereas these are the visuals when there are some participants in the meeting.

7a5ef60521d961cc.png

We can see 2 notable differences right off the bat:

  1. The status text changes from "No one else is here" to "[User] is in this call".
  2. We can see the images of users who have joined the call.

Both of these changes will work for us if we want to know if anyone has joined the meeting, but the latter has some possibilities to get information about the users who have already joined, so let's try to use that.

Open the "Elements Inspector" in chrome with the hotkey ( Mac: Cmd + Opt + C / Win: Ctrl + Shift + C) then click on the image of the user who has joined.

We can notice that there is an image with few class strings and the title attribute of the image has the name of the user who has joined the meeting. Moreover, this image tag is wrapped in a div with class U04fid. Once we add some joinees to our test meeting we can see that this parent div hosts multiple images(corresponding to different users).

So, we have a partial strategy in mind:

  1. Detect when the div with the class U04fid contains any elements.
  2. If it does, then that means that we have joinees in our meeting.
  3. Notify the user.

Let's get back to Gemini and ask how we can achieve this step by step.

Prompt:

How to detect for element changes in the HTML page?

It does give us a solution involving MutationObserver, but nothing specific to our requirement, let's try to modify the prompt.

Prompt:

How can I detect if an element "div.U04fid" has any child images in the HTML page?

This time we get a solution that doesn't include the MutationObserver but a solution that checks for the joinees using the parent div.

This is what we received:

const divU04fid = document.querySelector('div.U04fid');
const childImages = divU04fid.querySelectorAll('img');
if (childImages.length > 0) {
  // div.U04fid has at least one child image.
}

Let's try to modify our approach even more:

Prompt:

Create a method which checks if the element div.U04fid has any child elements,
if it does it should log it on the console. The method should be called checkJoinees
and it should be called as a callback for MutationObserver which is observing the document.body.

It returns with the following code snippet:

function checkJoinees(mutations) {
    for (const mutation of mutations) {
      if (mutation.type === 'childList') {
        // A child node was added or removed.
        if (document.querySelector('div.U04fid') && document.querySelector('div.U04fid').childElementCount > 0) {
          // div.U04fid has at least one child element.
          console.log('div.U04fid has at least one child element.');
        }
      }
    }
    return false;
}

const observer = new MutationObserver(checkJoinees);
observer.observe(document.body, {
  childList: true,
  delay: 1000
});

Copy this code into the content.js file, reload the extension(then reload the meet page).

Now when someone joins our meeting we are able to see the log statement in the console.

5. Send a notification to the user

Now that we are able to detect when a participant has joined the meeting, let's try to add the notification part in our chrome extension. We can browse the chrome extension's documentation or even tweak the prompts to know what we're looking for, but essentially we need to use the chrome.notifications.create API and the call to this method should be from a background service worker.

Prompt:

Using the documentation for chrome notifications tell me how to use the chrome.notifications.create method.

We see some detailed steps with the major highlights being:

  • Add the notifications permission in the manifest file.
  • Make a call to the chrome.notifications.create
  • The call should be in a background script.

In order to add a background script to a chrome extension in manifest version 3, we need the background.service_worker declaration in our manifest.json file.

So, we create a file called background.js and add the following to our manifest.json file.

"background": {
        "service_worker": "background.js"
},
"permissions": [
        "notifications"
]

With the above additions, our manifest file becomes:

{
    "name": "Meet Joinees Notifier",
    "version": "1.0",
    "manifest_version": 3,
    "description": "Adds the ability to receive a notification when a participant joins a Google Meet",
    "author": "<YOUR_EMAIL>",
    "content_scripts": [
        {
          "matches": ["https://meet.google.com/*"],
          "js": ["content.js"]
        }
    ],
    "background": {
        "service_worker": "background.js"
    },
    "permissions": [
            "notifications"
    ]
}

Prompt:

Create a method sendNotification that calls the chrome.notifications.create
method with the message, "A user joined the call" for a chrome extension with manifest v3,
the code is in the background service worker

Save this image in the root of your folder and rename it to success.png.

b2c22f064a3f2d9c.png

Then add the following code snippet to your background.js.

function sendNotification(notificationId, message) {
    chrome.notifications.create(notificationId, {
      type: "basic",
      title: "A user joined the call",
      message: message,
      iconUrl: "./success.png"
    });
}

sendNotification("notif-id", "test message");

Now reload the extension from the extensions page and you should immediately see a notification popup.

6. Add message passing in your chrome extension

Now, the last major step we need is to connect the content script's detection of a participant and the sendNotification method in the background script. In the context of chrome extensions the way to do that is via a technique called message passing.

This enables the communication between different parts of a chrome extension, in our case from the content script to the background service worker. Let's ask our friend Gemini how to achieve that.

Prompt:

How to send a message from the content script to the background script in a chrome extension

Gemini responds with relevant calls to chrome.runtime.sendMessage and chrome.runtime.onMessage.addListener.

Essentially we will use sendMessage to send a message from the content script that someone has joined the meet call and the onMessage.addListener as an event listener to react to the message sent by the content script. In this case we will trigger the call to the sendNotification method from this event listener.

We'll be passing in the notification message and an action property to the background service worker. The action property describes what the background script is responding to.

So, here's our content.js code:

function checkJoinees(mutations) {
    for (const mutation of mutations) {
      if (mutation.type === 'childList') {
        // A child node was added or removed.
        if (document.querySelector('div.U04fid') && document.querySelector('div.U04fid').childElementCount > 0) {
          // div.U04fid has at least one child element.
          sendMessage();
        }
      }
    }
    return false;
}

const observer = new MutationObserver(checkJoinees);
observer.observe(document.body, {
  childList: true,
  delay: 1000
});

function sendMessage() {
    chrome.runtime.sendMessage({
        txt: "A user has joined the call!",
        action: "people_joined"
    });
}

And this is our background.js code:

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === "people_joined") {
      sendNotification("notif-id", message.txt);
    }
  });
  

function sendNotification(notificationId, message) {
    chrome.notifications.create(notificationId, {
      type: "basic",
      title: "A user joined the call",
      message: message,
      iconUrl: "./success.png"
    });
}

Let's try to customize the notification message and get a unique notification id. For the notification message, we can include the user's name. If we recall from a previous step, we could see the name of the user in the title attribute of the image. So, we can fetch the name of the participant using document.querySelector('div.U04fid > img').getAttribute('title').

Regarding the notification id, we can fetch the tab ID of the content script and use that as a notification ID. This can be accomplished inside our event listener chrome.runtime.onMessage.addListener by making use of sender.tab.id.

Finally, our files should look something like this:

manifest.json

{
    "name": "Meet Joinees Notifier",
    "version": "1.0",
    "manifest_version": 3,
    "description": "Adds the ability to receive a notification when a participant joins a Google Meet",
    "author": "<YOUR_EMAIL>",
    "content_scripts": [
        {
          "matches": ["https://meet.google.com/*"],
          "js": ["content.js"]
        }
    ],
    "background": {
        "service_worker": "background.js"
    },
    "permissions": [
            "notifications"
    ]
}

content.js

function checkJoinees(mutations) {
    for (const mutation of mutations) {
      if (mutation.type === 'childList') {
        // A child node was added or removed.
        if (document.querySelector('div.U04fid') && document.querySelector('div.U04fid').childElementCount > 0) {
            const name = document.querySelector('div.U04fid > img').getAttribute('title');
            sendMessage(name);
        }
      }
    }
    return false;
}

const observer = new MutationObserver(checkJoinees);
observer.observe(document.body, {
  childList: true,
  delay: 1000
});

function sendMessage(name) {
    const joinee = (name === null ? 'Someone' : name),
        txt = `${joinee} has joined the call!`;

    chrome.runtime.sendMessage({
        txt,
        action: "people_joined",
    });
}

background.js

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === "people_joined") {
      sendNotification("" + sender.tab.id, message.txt); // We are casting this to string as notificationId is expected to be a string while sender.tab.id is an integer.
    }
  });
  

function sendNotification(notificationId, message) {
    chrome.notifications.create(notificationId, {
      type: "basic",
      title: "A user joined the call",
      message: message,
      iconUrl: "./success.png"
    });
}

7. Congratulations

In a short time we were able to build a chrome extension with the help of Gemini. Whether you are an experienced chrome extension developer, or new to the world of extensions,Gemini can help you with whatever tasks you want to achieve.

I would encourage you to ask about the different things you can do with a chrome extension. There are many APIs worth browsing like chrome.storage, alarms etc. Wherever you feel stuck, use Gemini or the documentation to know what you're doing wrong or to gather different ways to solve the problem.

It often takes modifying the prompt to get the help that is needed, but we can do so from one tab which retains all our contextual journey.