Wednesday, May 28, 2008

SELECT threads FROM GMail ORDER BY sender, subject, thread_activity

GMail is a very good mail service, it has a lot of useful features, mainly searching. However, it misses a very important feature: sorting. Someone may argue, why sort while you can find any message using fast search? I say, sometimes you want to view your threads sorted alphabetically when you don't know the exact phrases to search with.

I wrote a Javascript bookmarklet (GMail Sorter) to add sort controls in your GMail Inbox view. Once you enable the bookmarklet, you can see the following above your threads:



Clicking on the links asc/desc sorts visible threads by sender, subject or thread activity. Actually I wrote this bookmarklet mainly for the thread activity feature. Sometimes I don't have time to check community messages, until I come back after several days to see a lot of unread messages there. I wished there could be a way to sort them by their activity: threads that more people have replied on seems more interesting, I need to read them first.

The bookmarklet is heavily based on another bookmarklet which adds sort controls to all tables in page. Actually what I did was customizing that bookmarklet to work on GMail Inbox and adding different sort criteria. The sorting code is taken almost as is.

If you just need to use the bookmarklet and don't care about some implementation details, you can skip to section "How to install" below.

How GMail Sorter is different



Let me refer to that bookmarklet as B and mine as M. In B, all tables are tampered. In M only the Inbox table is tampered. It was a challenge finding the Inbox. Guys at Google seem to obfuscate ids and class names for all DOM elements. For example, they are something like 1d3f, 3xua,...
I attacked this issue by finding the "widest table". The Inbox table happens to be the 2nd widest table, where the notification area that appears on top of the table is the 1st widest one. However, the Inbox table does have an id and the notification area does not. So I find it by getting the widest table having an id:


function getWidestTable(tables) {
var maxTable = undefined;
var maxWidth = -1;
for (var i = 0; i < tables.length; i++) {
var table = tables[i];
if (table.clientWidth >= maxWidth && table.id != '') {
maxWidth = table.clientWidth;
maxTable = table;
}
}
return maxTable;
}


Applying B on GMail will give you an alert: "This page does not contain any tables". Wow, all these tables and B can't find it! The problem is that B searches for table tags in current document. In M, I had to iterate through all iframes and search in tables in their documents. Shortly I discovered that the Inbox table exists in an iframed named "canvas_iframe", so the code became a little simpler:


function getAllTables() {
g_tables = toArray(document.getElementsByTagName('table'));
var iframe = document.getElementById('canvas_frame');
if (!iframe) return null;
var iframedoc = iframe.contentDocument.document || iframe.contentWindow.document;
iframetables = toArray(iframedoc.getElementsByTagName('table'));
if (iframetables.length)
g_tables = g_tables.concat(iframetables);
if (!g_tables.length)
return null;
return g_tables;
}


How to install:


In Firefox, Just create a new bookmark and paste the following code in the URL:


javascript:function toArray (c) {var a, k;a = new Array;for (k=0; k < c.length; ++k)a[k] = c[k];return a;}function insAtTop(par,child) {if (par.childNodes.length)par.insertBefore(child, par.childNodes[0]);else par.appendChild(child);}function countCols(tab) {var nCols, i;nCols = 0;for(i = 0; i nCols)nCols = tab.rows[i].cells.length;return nCols;}function makeHeaderLink(colNo, ord, regex, numeric) {var link;link = document.createElement('a');link.href = '#';link.onclick = function() {var __st = window == top ? window : top;__st.sortTable(colNo, ord, regex, numeric);return false;};link.appendChild(document.createTextNode((ord>0)? 'asc':'desc'));return link;}function makeSortControl(header, col, title, regex, numeric) {header.appendChild(document.createTextNode(title + ' ['));header.appendChild(makeHeaderLink(col, 1, regex, numeric));header.appendChild(document.createTextNode('/'));header.appendChild(makeHeaderLink(col, -1, regex, numeric));header.appendChild(document.createTextNode(']'));}function makeHeader(nCols) {var header, headerCell, i;header = document.createElement('span');/*put links in columns 2 and 4 only (sender, subject)*/makeSortControl(header, 2, 'Sender(s)');header.appendChild(document.createTextNode(' | '));makeSortControl(header, 4, 'Subject');header.appendChild(document.createTextNode(' | '));makeSortControl(header, 2, 'Thread Activity', /\(\d*\)$/, true);return header;}function getWidestTable(tables) {var maxTable = undefined;var maxWidth = -1;for (var i = 0; i= maxWidth && table.id != '') {maxWidth = table.clientWidth;maxTable = table;}}return maxTable;}function getAllTables() {g_tables = toArray(document.getElementsByTagName('table'));var iframe = document.getElementById('canvas_frame');if (!iframe)return null;var iframedoc = iframe.contentDocument.document || iframe.contentWindow.document;iframetables = toArray(iframedoc.getElementsByTagName('table'));if (iframetables.length)g_tables = g_tables.concat(iframetables);if (!g_tables.length)return null;return g_tables;}(function () {/* ---- main() ---- ENTRY POINT HERE ------------------------- */g_tables = getAllTables();if (!g_tables) {alert("It seems that this script is not compatible with your gmail version (no tables), giving up!");return;}inboxtable = getWidestTable(g_tables);if (inboxtable.id.length<4) {alert("It seems that this script is not compatible with your gmail version (no inbox), giving up!");return;}var control = makeHeader(countCols(inboxtable));var tableparent = inboxtable.parentNode.parentNode;while(tableparent.previousSibling.style.display == 'none')tableparent = tableparent.previousSibling;tableparent.previousSibling.appendChild(control);/*tableparent.parentNode.insertBefore(control, tableparent);*/}) ();function compareRows(a,b) {if (a.sortKey == b.sortKey)return 0;return (a.sortKey < b.sortKey) ? g_order : -g_order;}function compareRowsNumeric(a,b) {if (a.sortKey == b.sortKey)return 0;return ((a.sortKey + '').match(/\d+/) - (b.sortKey + '').match(/\d+/)) * g_order;}function sortTable(colNo, ord, regex, numeric) {var table, rows, nR, bs, i, j, temp;g_order = ord;g_colNo = colNo;g_tables = getAllTables();table = getWidestTable(g_tables);rows = new Array();nR = 0;bs = table.tBodies;for (i = 0; i < bs.length; ++i)for(j=0; j < bs[i].rows.length; ++j) {rows[nR] = bs[i].rows[j];temp = rows[nR].cells[g_colNo];if (!temp)rows[nR].sortKey = '';else if (!regex)rows[nR].sortKey = temp.textContent.toLowerCase();else {var val = temp.textContent.toLowerCase().match(regex);rows[nR].sortKey = val ? val : '';}++nR;}if (numeric)rows.sort(compareRowsNumeric);else rows.sort(compareRows);for (i = 0; i < rows.length; ++i)insAtTop(table.tBodies[0], rows[i]);}


Put it in the Bookmarks Toolbar folder so that it is always visible. Anytime you need to enable the bookmarklet, open your Inbox view (or any messages view) and click on the bookmark you have just created. You will see a sort control added on top of the table. Play and enjoy!

How to modify:


If you are a Javascript geek and need to play with the code, here is a clear source where you can play with. After playing you will have to remove all new lines and prepend "javascript:" to form a valid URL that will fit in a bookmark. Don't worry, here is a one-liner to do this task:

(echo "javascript:" ; cat sorttables.js) | tr -d "\n\t" > sorttables_bookmarklet.js

This will read in the file sorttables.js that contains your code and outputs a file sorttables_bookmarklet.js where you can grasp its contents and paste it in your bookmark URL.

2 comments:

M@hdeTo said...

nice bookmarklet, its not working here though and i can't download the pretty version.

the first problem was this for(i = 0; i nCols), the second was somewhere near the iframedoc (can't find it again though :) ).. really nice blog, really nice posts. keep it up.

M@hdeTo said...

I finally reached this:
javascript:function toArray (c) {var a, k;a = new Array;for (k=0; k < c.length; ++k)a[k] = c[k];return a;}function insAtTop(par,child) {if (par.childNodes.length)par.insertBefore(child, par.childNodes[0]);else par.appendChild(child);}function countCols(tab) {var nCols, i;nCols = 0;for(i = 0; nCols && i < nCols; i++)nCols = tab.rows[i].cells.length;return nCols;}function makeHeaderLink(colNo, ord, regex, numeric) {var link;link = document.createElement('a');link.href = '#';link.onclick = function() {var __st = window == top ? window : top;__st.sortTable(colNo, ord, regex, numeric);return false;};link.appendChild(document.createTextNode((ord>0)? 'asc':'desc'));return link;}function makeSortControl(header, col, title, regex, numeric) {header.appendChild(document.createTextNode(title + ' ['));header.appendChild(makeHeaderLink(col, 1, regex, numeric));header.appendChild(document.createTextNode('/'));header.appendChild(makeHeaderLink(col, -1, regex, numeric));header.appendChild(document.createTextNode(']'));}function makeHeader(nCols) {var header, headerCell, i;header = document.createElement('span');/*put links in columns 2 and 4 only (sender, subject)*/makeSortControl(header, 2, 'Sender(s)');header.appendChild(document.createTextNode(' | '));makeSortControl(header, 4, 'Subject');header.appendChild(document.createTextNode(' | '));makeSortControl(header, 2, 'Thread Activity', /\(\d*\)$/, true);return header;}function getWidestTable(tables) {var maxTable = undefined;var maxWidth = -1;for (var i = 0; i= maxWidth && table.id != '';i++) {maxWidth = table.clientWidth;maxTable = table;}return maxTable;}function getAllTables() {g_tables = toArray(document.getElementsByTagName('table'));var iframe = document.getElementById('canvas_frame');if (!iframe)return null;var iframedoc = iframe.contentDocument.document || iframe.contentWindow.document;iframetables = toArray(iframedoc.getElementsByTagName('table'));if (iframetables.length)g_tables = g_tables.concat(iframetables);if (!g_tables.length)return null;return g_tables;}(function () {/* ---- main() ---- ENTRY POINT HERE ------------------------- */g_tables = getAllTables();if (!g_tables) {alert("It seems that this script is not compatible with your gmail version (no tables), giving up!");return;}inboxtable = getWidestTable(g_tables);if (inboxtable.id.length<4) {alert("It seems that this script is not compatible with your gmail version (no inbox), giving up!");return;}var control = makeHeader(countCols(inboxtable));var tableparent = inboxtable.parentNode.parentNode;while(tableparent.previousSibling.style.display == 'none')tableparent = tableparent.previousSibling;tableparent.previousSibling.appendChild(control);/*tableparent.parentNode.insertBefore(control, tableparent);*/}) ();function compareRows(a,b) {if (a.sortKey == b.sortKey)return 0;return (a.sortKey < b.sortKey) ? g_order : -g_order;}function compareRowsNumeric(a,b) {if (a.sortKey == b.sortKey)return 0;return ((a.sortKey + '').match(/\d+/) - (b.sortKey + '').match(/\d+/)) * g_order;}function sortTable(colNo, ord, regex, numeric) {var table, rows, nR, bs, i, j, temp;g_order = ord;g_colNo = colNo;g_tables = getAllTables();table = getWidestTable(g_tables);rows = new Array();nR = 0;bs = table.tBodies;for (i = 0; i < bs.length; ++i)for(j=0; j < bs[i].rows.length; ++j) {rows[nR] = bs[i].rows[j];temp = rows[nR].cells[g_colNo];if (!temp)rows[nR].sortKey = '';else if (!regex)rows[nR].sortKey = temp.textContent.toLowerCase();else {var val = temp.textContent.toLowerCase().match(regex);rows[nR].sortKey = val ? val : '';}++nR;}if (numeric)rows.sort(compareRowsNumeric);else rows.sort(compareRows);for (i = 0; i < rows.length; ++i)insAtTop(table.tBodies[0], rows[i]);}

but its probably not working due to themes ui changes and all
loved the idea though