The Rocky Road to my First Google Mail Extension for Greasemonkey

I’m using Google Mail since 2006 for my whole private e-mail communication, about 40 e-mails (exclude spam) each day. One of the nifty features I like is the possibility to add a number of free defined labels to conversations. Even though I’m using filters to do this automatically for the most common conversations normally at first I read an e-mail and then add labels addicted to the content and context. Something I miss was the possibility to select all labelled conversations within the thread list to archive them. After playing around with Greasemonkey for some year and developing some scripts for my employer I started to implement this feature for Google Mail. But it was a rocky road to success.

My first (simple) idea was to reuse ID’s of the html elements. But against my assumption Google doesn’t use ID’s. It’s just a mass of frames, divs and Javascript. Fortunately Google is on my side and provides an API (via Javascript functions) to access Google Mail interface by Greasemonkey. Because Greasemonkey support isn’t loaded by default for various reasons (i.e. performance) API has to be initialized through call.

// call is asynchronous and needs a callback function
unsafeWindow.gmonkey.load("1.0", function() {
 ...
});

Anyway this doesn’t work for me more often than it did. The loading time is highly variable. Even the provided example by Google Gmail View Watcher runs once in a while. On my search for a solution I found a blog post about using the GMail Greasemonkey API by Eric Biven.

window.addEventListener('load', function() {
 if (unsafeWindow.gmonkey) {
  unsafeWindow.gmonkey.load('1.0', init);
 }
}, true);

function init(g) {
 gmail = g;
 // Calls to the gmail API seem to fail far less often if you wait
 // for a bit to actually start using it.
 window.setTimeout(function() {
  try {
   ...
  } catch (ex) {
   // This seems like a brutal hack, however the call to getCanvasElement
   // will sometimes fail when the page loads.  If that happens this seems
   // to resolve it eventually.
   window.location.reload();
  }
 }, 500);
}

His workaround works fine. From this point development was easy even though I’m not a Javascript specialist.

// ==UserScript==
// @name           Google Mail select labelled conversations
// @description	   Add selection functionality for labelled conversation within the thread list to Google Mail
// @include        http://gmail.google.com/*
// @include        https://gmail.google.com/*
// @include        http://mail.google.com/*
// @include        https://mail.google.com/*
// ==/UserScript==

var gmail = null;
var CONS_LBL_CLS = 'labelled';

window.addEventListener('load', function() {
 if (unsafeWindow.gmonkey) {
  unsafeWindow.gmonkey.load('1.0', init);
 }
}, true);

function init(g) {
 gmail = g;
 // Calls to the gmail API seem to fail far less often if you wait
 // for a bit to actually start using it.
 window.setTimeout(function() {
  try {
   modifyThreadList();
   gmail.registerViewChangeCallback(modifyThreadList);
  } catch (ex) {
   // This seems like a brutal hack, however the call to getCanvasElement
   // will sometimes fail when the page loads.  If that happens this seems
   // to resolve it eventually.
   window.location.reload();
  }
 }, 500);
}

getElementsByClassName = function(node, cls) {
 var returnNode = [];
 var myclass = new RegExp('\\b'+cls+'\\b');

 var elem = node.getElementsByTagName('*');
 for (var i = 0; i < elem.length; i++) {
  var classes = elem[i].className;
  if (myclass.test(classes)) returnNode.push(elem[i]);
 }
 return returnNode;
}; 

modifyThreadList = function() {
 if(gmail.getActiveViewType() == 'tl') {
  var view = gmail.getActiveViewElement();
  var selGrp = getElementsByClassName(view, 'yU');

  var sel;
  for(var i = 0; i<selGrp.length; i++) {
   sel = new SelectionGroup(selGrp[i]);
   if(!sel.hasLabelledLink()) {
    sel.addLabelledLink();
   }
  }
 }
}

SelectionGroup = function(selGrp) {

 this.hasLabelledLink = function() {
  var childs = selGrp.childNodes;
  for(var i=0; i<childs.length; i++) {
   if(childs[i].className == CONS_LBL_CLS) {
    return true;
   }
  }
  return false;
 },

 this.addLabelledLink = function() {
  selGrp.appendChild(document.createTextNode(', '));

  var lbl = document.createElement('span');
  lbl.className = CONS_LBL_CLS;
  lbl.innerHTML = 'Labelled';
  lbl.addEventListener('click', selectLabelledConversations, true);
  selGrp.appendChild(lbl);
 }
}

selectLabelledConversations = function() {
 var view = gmail.getActiveViewElement();

 // maybe you are using multiple threadlists
 var convGrp = getElementsByClassName(view, 'RnTrV');
 var rConvGrp = convGrp[0];
 if(!rConvGrp) {
  return;
 }

 var conv;
 var convs = rConvGrp.getElementsByTagName('tr');
 for(var i=0; i<convs.length; i++) {
  conv = new Conversation(convs[i]);
  if(conv.hasLabel()) {
   conv.select();
  }
 }
};

Conversation = function(conv) {

 this.hasLabel = function() {
  var lbls = getElementsByClassName(conv, 'as');
  return (lbls.length == 0) ? false : true;
 },

 this.select = function() {
  var selBox = conv.getElementsByTagName('input');
  selBox[0].click();
 }
};

If you find any improvements feel free to write a comment. If you are interested in this extension you can download it here.

Thank you for listening!

About this entry

  • Published: 05/25/09 / 3pm
  • Category: Articles
  • Tags: , , , ,
  • Share