saidit

By chromakode Last update Jan 24, 2009 — Installed 135 times. Daily Installs: 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0

There are 7 previous versions of this script.

// ==UserScript==
// @name          	saidit
// @description   	Emphasizes unread comments on reddit.com.
// @include       	http://*.reddit.com/*comments/*
// @version       	0.4
// ==/UserScript==

//
// Copyright (C) 2009 Chromakode
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

var PROFILE = false;
if (PROFILE) {
  var startTime = Date.now();
}

//
// === Globals ===
//

// Keep SLOT_COUNT "slots" for seen comment storage, freeing storage of the oldest slot if none are empty.
// This prevents disk storage from slowly creeping up for hardcore redditors.
const SLOT_COUNT = 2500;

var pageThingID = getThingID(getPageThing());
if (!pageThingID) { return; } // Bail if for some reason we can't get an id for the page

var localStorage;
if (!localStorage) {
  localStorage = globalStorage.namedItem(window.location.hostname);
}
var seenArray = [];
var seenObject = {};

//
// === Utility functions ===
//
function getChildByClassName(element, className) {
  var res = element.ownerDocument.evaluate('descendant::*[contains(concat(" ",normalize-space(@class), " "), " '+className+' ")]', element, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  return res.singleNodeValue;
}

function getThingID(thingElement) {
  var match;
  match = match || thingElement.className.match(/\s*id-(\S+)\s*/); // New method
  match = match || thingElement.id.match(/thingrow\_(\S+)/); // Old method
  if (match) {
    return match[1];
  } else {
    return null;
  }
}

function getPageThing() {
  return document.getElementsByClassName("link")[0] || document.getElementsByClassName("linkcompressed")[0];
}

function isCollapsed(commentElement) {
  var collapsed = getChildByClassName(commentElement, "collapsed");
  return collapsed.style.display != "none";
}

function isVisible(element) {
  return element.clientHeight != 0;
}

function eachWithClass(cls, callback, parent) {
  if (parent === undefined) { parent = document }
  var comments = parent.getElementsByClassName(cls);
  
  Array.prototype.forEach.call(comments, function(commentElement) {
    callback(commentElement);
  });
}

function eachComment(callback, parent) {
  eachWithClass("comment", callback, parent);
}

//
// === Main logic functions ===
//
function loadSeen() {
  var seenData = localStorage.getItem("seen-"+pageThingID);
  // XXX Strangely, if you remove a Storage item in another tab, it ends up as "" in the other tab, and if removed, remains as "". Bug in Mozilla code?
  if (seenData == null || seenData == "") {
    // No stored seen comment data
    var slot = (GM_getValue("lastSlot", -1) + 1) % SLOT_COUNT;
    
    // Free the slot, if filled
    var oldID = GM_getValue("slot-"+slot);
    if (oldID !== undefined) {
      localStorage.removeItem("seen-"+oldID);
    }
    
    GM_setValue("slot-"+slot, pageThingID);
    GM_setValue("lastSlot", slot);
  } else {
    // Load the seen data
    seenArray = seenData.value.split(",");
    
    // Build an object for faster access
    seenArray.forEach(function(element) {
      seenObject[element] = true;
    });
  }
}

function saveSeen() {
  localStorage.setItem("seen-"+pageThingID, seenArray.join(","));
}

function checkComment(commentElement) {
  if (isVisible(commentElement)) {
    if (isCollapsed(commentElement)) {
    
      // When a collapsed element is expanded, check it and its children.
      var expand = getChildByClassName(commentElement, "expand");
      function update() {
        expand.removeEventListener("click", update, false);
        checkComment(commentElement);
        eachComment(checkComment, commentElement);
      }
      expand.addEventListener("click", update, false);
      
    } else {
    
      // The comment is visible and not collapsed.
      // Display it as unread, and mark it as read.
      var commentID = getThingID(commentElement);
      if (seenObject[commentID] != true) {
        commentElement.className += " unread";
        seenArray.push(commentID);
      }
      
    }
  }
}

//
// === Main ===
//
loadSeen();
eachComment(checkComment);
saveSeen();

GM_addStyle(".comment.unread .tagline { font-weight:bold; background-color:#fea; display:inline; }");

document.addEventListener("DOMNodeInserted", function(e) {
  if (e.target.className && e.target.className.match(/comment/)) {
    checkComment(e.target);
  }
}, false);

window.addEventListener("beforeunload", function() {
  saveSeen();
}, true);

if (PROFILE) {
  var endTime = Date.now();
  alert((endTime - startTime) + " ms");
}