Ontogeny of a chrome extension: the tab notifier

The tab notifier is our solution for who uses messaging web clients but don’t want to renounce nice toasts popping up as alerts. Let’s see how it’s done, in order to be able to customize it or to learn something that may be useful to make other extensions.
To develop a chrome extension you need to know javascript and few more things.
Let’s start from the begin.
We need to create at least a manifest.json file and a html file with the javascript code that will be run in background. A first simple technique that we can use to detect title changes is to compare every few seconds the title of every tab of every window with the previous value. We already know that we are going to do it in a better way, but getting the list of the tabs is definitely one thing that can turn out useful, so let’s start from here.

Here’s the file manifest.json:

{
	"background_page": "background.html",
	"description": "Notifies when the title of a tab changes",
	"name": "Tab notifier",
	"permissions": [ "tabs" ],
	"version": "0.1"
}

Note that we need the tabs permission (as every time we need to use APIs from the chrome.windows or chrome.tabs namespace). This makes a warning show up when you install the extension from a compressed .crx file, but not when you install it form an unpacked directory (which it’s what we are going to do as long we are still developing it).

Here’s the file background.html:

<html><head><script>

var pollInterval = 1000 * 20;  // 20 seconds
var globalTabs = {};

function populateTabs(winList) {
    var newTabs = {}; //scanned tabs
    for (x=0;x<winList.length;x++) {
		var win=winList[x];
		win.tabs.forEach(function(tab) {
			if (tab.title) {
				var prevTab=globalTabs[tab.id];
				if (prevTab!==undefined && prevTab.title!=tab.title && prevTab.url==tab.url && (!tab.selected || !win.focused)) {
					var popup = window.webkitNotifications.createNotification(tab.favIconUrl ? tab.favIconUrl : "", "", tab.title);
					popup.show();
				}
				newTabs[tab.id]={title:tab.title, url:tab.url};
			}
		});
    }
	globalTabs=newTabs;
}

function check()
{
	chrome.windows.getAll({'populate' : true}, populateTabs);
	window.setTimeout(check, pollInterval);
}

</script></head><body onload="check()"></body></html>

The background page is loaded when the plug-in is started or reload, and it contain only the javascript code bound to the load event of the page. This code scans all the tabs every 20 seconds, please note from the row 13 that the pop up is showed only when a title is changed but the url is not (which means that the title has been changed by some javascrit code because the page is still the same) and only if the tab is not opened and the windows focused (that’s it only if the user is not seeing what it’s happening).
To try the extension go to the page chrome://extensions/, active the “Developer mode” and click on “Load unpacked extension…”, going to the directory where our two files are.
To test it more conveniently you may need a small html page like this. It generates a scrolling title (which ideally is one of the things that we wouldn’t like to cause a notification but it’s perfect for our aim because it keeps on generating the event that we are trying to detect, anyway we’ll try to distinguish a scrolling title from an actual message notification, in some cases, later on, not showing it a second time after we close it, refer to cyclic messages detection). We can experiment some defects easy predictable. Definitely the more obvious is that we can have a delay after the tile change. This can be acceptable when we receive an email, but can be intolerable when we receive a instant message. We can play with the pollInterval variable, however, another defects is that a script has frequently to go through all the tabs even when nothing is happening, and decreasing pollInterval would make things worse.

I believe in a better way (content scripts).

Instead of polling every tab we can proceed the opposite way. We can use the DOMSubtreeModified events in every tab we want to monitor to detect at once every change of the title. This is possible because chrome allows us to inject javascript in the pages. Since this injected javascript can access only to a small subset of the chrome APIs, we need to send a message to the background page and let it do the rest. To inject javascript code we have to add the content_scripts permission and the new field content_scripts to the manifest.json file:

{
	"content_scripts": [
		{
			"matches": ["http://*/*", "https://*/*"],
			"js": ["tabnot.js"]
		}
	],
   "background_page": "background.html",
   "description": "Notifies when the title of a tab changes",
   "name": "Tab notifier",
   "permissions": [ "tabs", "notifications" ],
   "version": "0.2"
}

our content_scripts field says that the javascript in tabnot.js will be injected in every page (but just if it’s the top frame) whose the url matches “http://*/*” or “https://*/*” (that’s for every page in a web site). This code will run inside an isolated world, so we don’t have to worry about clashes with any javascript in the monitored pages.

The content of tabnot.js file is:

var titleEl = document.getElementsByTagName("title")[0];
var docEl = document.documentElement;

if (docEl && docEl.addEventListener) {
	docEl.addEventListener("DOMSubtreeModified", function(evt) {
		var t = evt.target;
		if (t === titleEl || (t.parentNode && t.parentNode === titleEl)) {
			titleModified();
		}
	}, false);
} 

function titleModified() {
	chrome.extension.sendRequest("");
}

We’ll make more complex the function titleModifier to fit the needs will occur. Now it’s pretty simple: it send a message to the background page (as we said the content scripts can’t use all of the chrome APIs but can, off course, communicate with other parts of the extension).
This is the new background.html:

<html><head><script>

function init() {
	chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
		var tab=sender.tab;
		chrome.windows.get(tab.windowId, function(win) {
			if (tab.selected && win.focused) return;
			var popup = window.webkitNotifications.createNotification(tab.favIconUrl ? tab.favIconUrl : "", "", tab.title);
			popup.show();
		});

	});
}

</script></head><body onload="init()"></body></html>

The _request_ parameter of the callback function passed as listener will be always an empty string according with the sendRequest inside tabnot.js. However we have all the information we need inside sender.tab.
Please note that, unlike what we could do with the previous method, when after any change to the code we needed to reload only the extension, now we need to refresh also the monitored tab in order to reload the injected javascript.
Testing it with this page we’ll see that that shortly the screen will be filled with notifications. Probably it’s better if every tab can trigger just one notification each time, and that if the title changes again while a notification is still open the old notification will be replaced accordingly. In this way scrolling titles and cyclic messages will be readable the same way you can do it on the tab.
The chrome API offer a simple way to achieve that, the property replaceId of the notification object. We set it before showing the popup and when we try to show it and a popup with the same value of replaceId is already open then the old one will be replaced. The API documentation says that the id of a tab is unique in a session, then we can use it as replaceId value.
Another desirable feature is that capability to open the tab when you click on the popup showed by it. Then we’ll close the popup because its job is done.Here’s the third version of the background page:

<html><head><script>

function init() {
	chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
		var tab=sender.tab;
		chrome.windows.get(tab.windowId, function(win) {
			if (tab.selected && win.focused) return;
			var popup = window.webkitNotifications.createNotification(tab.favIconUrl ? tab.favIconUrl : "", "", tab.title);
			popup.onclick=function() {
				this.cancel();
				chrome.tabs.update(tab.id, {'selected' : true}, null);
				chrome.windows.update(tab.winowId, {'focused' : true}, null);
			}
			popup.replaceId=tab.id;
			popup.show();
		});

	});
}

</script></head><body onload="init()"></body></html>

Cyclic messages detection

We have built something that detects promptly every change of title in the tabs but we’d like to show a notification only when it’s useful (and not annoying…).
Let’s consider for instance a case that can be applied to several web sites. When an instant message arrives, Facebook changes the title alternatively from the original value (e.g. “Facebook”) to something like “Charlie Brown messaged you” and vice versa. The user may not be interested in the message, and she can just choose to close the notification, but, if we simply show the notification every time a title change (as we have done so far), the notification will keep on showing up. To avoid this we may use an algorithm as complex as we like, but we are going to restrict the scenario to the case of messages showed always in the same order and all different from each other, which should cover all of the relevant eventualities.
We use an array containing the titles that could be part of a cyclic message.
If the title changes twice within 15 seconds we push the titles in the array, but if the title changes slower we empty the array.
As long as a change occurs fast enough we compare the new title with an old one (accordingly with an index increased every time there is a match and reset when there isn’t) and every time there isn’t a match we push it in the array. When the title matches the very last element of the array the boolean cycle is set to true. The comparison goes on with the following changes of the title and cycle will be reset the first time a new title doesn’t match an old one. If you close a popup during a sequence detection every new change to the title while cycle is true won’t trigger a new notification.
The cyclic messages detection can be implemented in the content script or in the background page. Every choice has its own pro and con but they are not a big deal. In the current version (1.0) of the tab notifier the detection is implemented in the content script (tabnot.js) and the requests sent to the background page contains a string that indicates whether to show or to hide the notification, while the background page distinguishes the reason of the closing of a popup and when it’s due to a click of the user on the closing icon (that’s, if you look into the code, when popup.closingReason is undefined) it uses the API chrome.tabs.sendRequest to communicate it to the content script.

Other improvements

To see the full code you can, after installing the extension, go to the directory where the files are stored by chrome, that should be ~/.config/google-chrome/Default/Extensions/ on linux, something like C:\Users\*UserName*\AppData\Local\Google\Chrome\User Data\Default\Extensions in a recent windows or C:\Documents and Settings\*UserName*\Local Settings\Application Data\Google\Chrome\User Data\Default\Extensions in an older version of windows.
The code is a bit more complex compared with the one in this post. In fact, beside the cyclic messages detection, it implements some small extra ideas. For instance it reproduces the behaviour of a IM client also when it hides an open notification when the tab that triggered it is opened manually (this is done by means of the events onSelectionChanged and onFocusChanged).

What we think is really important is to leave the user free to decide what has to be notified and what must not. Which actually is another excuse to introduce another magic of the chrome extensions: the context menus.

Let’s change again the manifest:

{
	"content_scripts": [
		{
			"matches": ["http://*/*", "https://*/*"],
			"js": ["tabnot.js"]
		}
	],
	"icons": {
		"16": "bell.png"
	},
   "background_page": "background.html",
   "description": "Notifies when the title of a tab changes",
   "name": "Tab notifier",
   "permissions": [ "tabs", "notifications", "contextMenus" ],
   "update_url": "http://unusoft.it/extensions/updates.xml",
   "version": "1.0.0"
}

The highlighted changes show a new permission (contextMenus) that allows us to use the APIs in the chrome.contextMenus namespace. The icon 16×16 is showed next to a new “Tab notifier” item on the context menu by chrome. Just right-click on a web page too see it.
In the background page we create a checkbox item in the context menu of chrome titled “Ignore this domain for the current session” with chrome.contextMenus.create during the initialization and we check or un-check it by means of chrome.contextMenus.update whenever the user selects a different tab. When the user clicks the menu item the extension updates a set of blocked hostnames. Here’s the creation of the item:

var menuItemId=chrome.contextMenus.create({type:"checkbox",
	title:"Ignore this domain for the current session",
	checked:true,
	contexts:["all"],
	documentUrlPatterns:["http://*/*", "https://*/*"],
	onclick:function(info,tab){
		var hostname = getHostname(tab.url);
		if (hostname) {
			if(info.checked) {
				mutedDomains[hostname]=true;
			} else {
				delete mutedDomains[hostname];
			}
		}
	}
});	

getHostname is a simple function that extracts the hostname by mean of a regular expression. contexts determines the types of elements of the page that will have the item in their context menu, documentUrlPatterns limits the documents that will have the item in their context menu on the basis of the url, in our case it’s equal to the content_scripts.matches field of the manifest, that limits the monitored pages. Here’s the function used to update the menu item:

function updateMenu(url) {
	var hostname = getHostname(url);
	chrome.contextMenus.update(menuItemId,{checked:hostname.length>0 && (hostname in mutedDomains)});
}

Ideas for the future

Maybe you don’t like the matter that the list of ignored domains are not kept stored between two consecutive sessions. Indeed, doing that would be easy enough thanks to a new feature of Html5, localStorage. But I wouldn’t do that until we enable the user to see and edit directly the list. We can do that adding an option page, and with that we can do even more like managing regular expressions to ignore groups of url or subdomains (besides to ignore single urls). The list should be initially filled with a default set of web sites that already use desktop notifications.
Html5 audio may helps to add another cool feature, that’s configurable audio notifications (that have to be disabled for a configurable list of web sites, initially filled with well-known sites that already play sounds when they have something to notify.

Update (21/12/2011): by the time you read this post some of the listed features will have been implemented.
Please check the changelog

Leave a Reply

Your email address will not be published. Required fields are marked *