Gmail + Google Reader

By Mihai Parparita Last update Oct 15, 2006 — Installed 6,118 times.
// ==UserScript==
// @name           Gmail + Google Reader
// @namespace      http://persistent.info/
// @description    Embeds Google Reader into Gmail by adding a "Feeds" link
// @include        http://mail.google.com/*
// @include        https://mail.google.com/*
// @include        http://www.google.com/reader/*
// ==/UserScript==

var readerFrameNode = null;
var feedsContainerNode = null;
var hiddenNodes = [];

const GMAIL_STYLES = [
  "#reader-frame {",
  "  width: 100%;",
  "  border: 0;",
  "}",
  
  ".reader-embedded #ft {",
    "display: none",
  "}",
  
  // Make currently selected item appear normal
  ".reader-embedded table.cv * {",
  "  background: #fff;",
  "  font-weight: normal",
  "}",
  
  // Make the feeds link appear selected
  ".reader-embedded #feeds-container {",
  "  background: #c3d9ff;",
  "  -moz-border-radius: 5px 0 0 5px;",
  "  font-weight: bold;",
  "  color: #00c;",
  "}",
].join("\n");

const READER_STYLES = [
  "body {",
  "  background: #fff",  
  "}",
  
  "#nav,",
  "#logo-container,",
  "#global-info,",
  "#viewer-header {",
  "  display: none !important;",
  "}",
  "",
  "#main {",
  "  margin-top: 0;",
  "}",
  "",
  "#chrome {",
  "  margin-left: 0;",
  "}"
].join("\n");

const READER_UNREAD_COUNT_URL = 
  "http://www.google.com/reader/api/0/unread-count?" +
  "all=true&output=json&client=gm";

const READER_LIST_VIEW_URL =
  "http://www.google.com/reader/view/user/-/state/com.google/reading-list?" +
  "gmail-embed=true&view=list";

if (document.location.hostname == "mail.google.com") {
  injectGmail();
} else if (document.location.hostname == "www.google.com" &&
           document.location.search.indexOf("gmail-embed") != -1) {
  injectReader();
}           

function injectGmail() {
  if (!getNode("nds") || !getNode("ds_inbox")) return;
  
  GM_addStyle(GMAIL_STYLES);
  
  var feedsNode = newNode("span");
  feedsNode.className = "lk";
  feedsNode.innerHTML = 
      'Feeds ' +
      '<span id="reader-unread-count"></span>';
  feedsNode.onclick = showReaderFrame;
  
  feedsContainerNode = newNode("div");
  feedsContainerNode.className = "nl";
  feedsContainerNode.id = "feeds-container";
  feedsContainerNode.appendChild(feedsNode);
  
  var navNode = getNode("nds");
  
  navNode.insertBefore(feedsContainerNode, navNode.childNodes[2]);
  
  window.addEventListener("resize", resizeReaderFrame, false);
  
  updateUnreadCount();
  
  window.setInterval(updateUnreadCount, 5 * 60 * 1000); 
  
  window.setInterval(checkFeedsParent, 1000);
}

function checkFeedsParent() {
  var navNode = getNode("nds");
  
  if (feedsContainerNode.parentNode != navNode) {
    navNode.insertBefore(feedsContainerNode, navNode.childNodes[2]);
  }  
}

function updateUnreadCount() {
  var unreadCountNode = getNode("reader-unread-count");
  
  if (!unreadCountNode) return;
  
  GM_xmlhttpRequest({
    method: "GET",
    url: READER_UNREAD_COUNT_URL,
    onload: function(details) {
      var data = eval("(" + details.responseText + ")");
      var isUnread = false;
      
      for (var i = 0, unreadCountPair; 
           unreadCountPair = data.unreadcounts[i]; 
           i++) {
        if (unreadCountPair.id.indexOf("reading-list") != -1) {
          var count = unreadCountPair.count;
          if (count == 0) break;
          
          unreadCountNode.innerHTML = 
              " (" + count + (count == data.max ? "+" : "") + ") ";
          isUnread = true;
          break;
        }
      }
      
      if (!isUnread) {
        unreadCountNode.innerHTML = "";
      }
    }
  });
}

function resizeReaderFrame() {
  if (!readerFrameNode) return;
  
  readerFrameNode.style.height = 
      (window.innerHeight - readerFrameNode.offsetTop) + "px";  
}

function showReaderFrame(event) {
  var container = getNode("co");
  
  addClass(document.body, "reader-embedded");
  
  hiddenNodes = [];
  
  for (var i = container.firstChild; i; i = i.nextSibling) {
    hiddenNodes.push(i);
    i.style.display = "none";
  }
  
  readerFrameNode = newNode("iframe");
  readerFrameNode.src = READER_LIST_VIEW_URL;
  readerFrameNode.id = "reader-frame";
  
  container.appendChild(readerFrameNode);
  
  container.parentNode.style.paddingRight = "0";
  container.parentNode.style.paddingBottom = "0";
  
  resizeReaderFrame();
  
  // Make clicks outside the content area hide it  
  getNode("nav").addEventListener("click", hideReaderFrame, false);
  
  // Since we're in a child of the "nav" element, the above handler will get
  // triggered immediately unless we stop this event from propagating
  event.stopPropagation();  
  
  return false;
}

function hideReaderFrame() {
  var container = getNode("co");

  container.removeChild(readerFrameNode);    
  readerFrameNode = null;
  
  for (var i=0; i < hiddenNodes.length; i++) {
    hiddenNodes[i].style.display = "";
  }
  getNode("nav").removeEventListener("click", hideReaderFrame, false);  
                                     
  removeClass(document.body, "reader-embedded");      
  
  container.parentNode.style.paddingRight = "1ex";
  container.parentNode.style.paddingBottom = "1ex";
  
  return true;
}

function injectReader() {
  GM_addStyle(READER_STYLES);  
}

// Shorthand
function newNode(type) {return unsafeWindow.document.createElement(type);}
function newText(text) {return unsafeWindow.document.createTextNode(text);}
function getNode(id) {return unsafeWindow.document.getElementById(id);}

function hasClass(node, className) {
  return className in getClassMap(node);
}

function addClass(node, className) {
  if (hasClass(node, className)) return;
  
  node.className += " " + className;
}

function removeClass(node, className) {
  var classMap = getClassMap(node);

  if (!(className in classMap)) return;
  
  delete classMap[className];
  var newClassList = [];
  
  for (var className in classMap) {
    newClassList.push(className);
  }
  
  node.className = newClassList.join(" ");
}

function getClassMap(node) {
  var classMap = {};
  var classNames = node.className.split(/\s+/);
  
  for (var i = 0; i < classNames.length; i++) {
    classMap[classNames[i]] = true;
  }
  
  return classMap;
}