theoky / History of the Seen

// ==UserScript==
// @name History of the Seen
// @namespace https://github.com/theoky/HistoryOfTheSeen
// @description Script to implement a history of the seen approach for some news sites. Details at https://github.com/theoky/HistoryOfTheSeen
// @author          Theoky
// @version	        0.4192
// @lastchanges     workaround for bug in GreaseMonkey 3.2
// @license         GNU GPL version 3
// @released        2014-02-20
// @updated         2014-06-10
// @homepageURL   	https://github.com/theoky/HistoryOfTheSeen
//
// @grant      GM_getValue
// @grant      GM_setValue
// @grant      GM_deleteValue
// @grant      GM_registerMenuCommand
// @grant      GM_listValues
// @grant      GM_addStyle
//
// for testing purposes (set FireFox greasemonkey.fileIsGreaseable) 
// @include file://*testhistory.html
//
// @include http*://*.derstandard.at/*
// @include http*://*.faz.net/*
// @include http*://*.golem.de/*
// @include http*://*.handelsblatt.com/*
// @include http*://*.heise.de/newsticker/*
// @include http*://*.kleinezeitung.at/*
// @include http*://*.nachrichten.at/*
// @include http*://*.oe24.at/*
// @include http*://*.orf.at/*
// @include http*://orf.at/*
// @include http*://*.reddit.com/*
// @include http*://*.spiegel.de/*
// @include http*://*.sueddeutsche.de/*
// @include http*://*.welt.de/*
// @include http*://*.wirtschaftsblatt.at/*
// @include http*://*.zeit.de/*
// @include http*://dastandard.at/*
// @include http*://derstandard.at/*
// @include http*://diepresse.com/*
// @include http*://diestandard.at/*
// @include http*://kurier.at/*
// @include http*://slashdot.org/*
// @include http*://taz.de/*
// @include http*://notalwaysright.com/*
// @include http*://www.nytimes.com/*

// @require http://code.jquery.com/jquery-2.1.1.min.js
// @require http://code.jquery.com/ui/1.11.2/jquery-ui.js
// @require https://greasyfork.org/scripts/130-portable-md5-function/code/Portable%20MD5%20Function.js?version=10066
// was require md5.js 
// was require http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js
// ==/UserScript==

// Copyright (C) 2015  T. Kopetzky - theoky
//
// 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/>.
//
// Tested with FireFox 34 and GreaseMonkey 2.3

//-------------------------------------------------
//Functions

(function(){

	var defaultSettings = {
			ageOfUrl: 5, 	// age in days after a url is deleted from the store
							// < 0 erases all dates (disables history)
			targetOpacity: 0.3,
			targetOpacity4Dim: 0.85,
			steps: 10,
			dimInterval: 30000,
			expireAllDomains: true,		// On fast machines this can be true and expires
							// all domains in the database with each call. If false,
							// only the urls of the current domain are expired which 
							// is slightly faster.
			cleanOnlyDaily: true,
			considerViewPort: true,
			dbOpsPerRun: 5
		};

	var UNDEF = 'undefined';
	var DEFAULT_TAG = 'a';
	var defaultGetContentFct = function(elem) {
		if ((typeof elem != UNDEF) && (typeof elem.href != UNDEF)) {
			return elem.href;
		}
		return UNDEF;
	};
	var AFTER_SCROLL_DELAY = 750;
	var DO_DEBUG = false;
	var DEBUG_LVL_ERROR = 1;
	var DEBUG_LVL_WARN = 2;
	var DEBUG_LVL_INFO = 4;
	
	var perUrlSettings = [
  		{
			url : ['.*\.?slashdot\.org' ],
			tag : 'article',
			upTrigger: "../article",
			getContent: function(elem) {
				if ((typeof elem != UNDEF) && (typeof elem.id != UNDEF)) {
					return elem.id;
				}
				return UNDEF;
			},
			parentHints : [ ] 
		},

		{
			url : ['.*\.?derstandard\.at', '.*\.?diestandard\.at', '.*\.?dastandard\.at' ], 
			upTrigger: "../a",
			parentHints : [
					"ancestor::div[contains(concat(' ', @class, ' '), ' text ')]",
					"ancestor::ul[@class='stories']" ]
		},

		{
			url : ['notalwaysright\.com'],
			upTrigger: "../a[@rel='bookmark']",
			parentHints : [ "ancestor::div[contains(concat(' ', @class, ' '), ' post ')]" ]
		},

		{
			url : ['.*\.?golem.de'],
			upTrigger: "../a",
			parentHints : [ "ancestor::li",
					"ancestor::section[@id='index-promo']",
					"ancestor::section[contains(concat(' ', @class, ' '), ' promo ')]" ]
		},

		{
			url : ['.*\.?reddit.com'],
			// class="title may-blank  srTagged imgScanned"
			upTrigger: "../a[contains(@class, 'title') and contains(@class, 'may-blank')]",
			parentHints : [ "ancestor::div[contains(concat(' ', @class, ' '), ' thing ')]" ]
		},
		
		{
			url : ['nytimes\.com'],
			upTrigger: "../a",
			parentHints : [
					// "ancestor::li[contains(concat(' ', @class, ' '), ' portal-post ')]",
					"ancestor::div[contains(concat(' ', @class, ' '), ' collection ')]"
					]
		}		
	];
	
	var dimMap = {};
	var countDownTimer = defaultSettings.steps;
	var theHRefs = null;
	var curSettings = null;
	var KEY_LAST_EXPIRE_OP = "lastExpire";
	var timeOutAfterLastScroll = UNDEF;
	var tag2Process = null;
	var getContentFct = null;
	var theDomain = null;

	var progressbar;
	var progressLabel;	

	// Styling 
	var progressBarStyle = 
		".ui-widget {" + 
		"	font-family: Verdana,Arial,sans-serif !important;" + 
		"	font-size: 1.1em !important;" + 
		"}" + 
		".ui-widget-content {" + 
		"	border: 1px solid #aaaaaa !important;" + 
		"	color: #222222 !important;" + 
		"}" + 
		".ui-widget-header {" + 
		"	border: 1px solid #aaaaaa !important;" + 
		"	background: #cccccc   !important;" + 
		"	color: #222222 !important;" + 
		"	font-weight: bold !important;" + 
		"}" + 
		".ui-progressbar {" + 
		"	height: 2em !important;" + 
		"	text-align: left !important;" + 
		"	overflow: hidden !important;" + 
		"}" + 
		".ui-progressbar .ui-progressbar-value {" + 
		"	margin: -1px !important;" + 
		"	height: 100% !important;" + 
		"}" + 
		".ui-progressbar .ui-progressbar-overlay {" + 
		"	background: url('data:image/gif;base64,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') !important;" + 
		"	height: 100% !important;" + 
		"	filter: alpha(opacity=25) !important; /* support: IE8 */" + 
		"	opacity: 0.25 !important;" + 
		"}" + 
//		".ui-progressbar-indeterminate .ui-progressbar-value {" + 
//		"	background-image: none !important;" + 
//		"}" + 

		".ui-progressbar {" + 
		"	height: 2em !important;" + 
		"	text-align: left !important;" + 
		"	overflow: hidden !important;" + 
		" 	position: absolute !important;" + 
		" 	left: 20% !important;" + 
		" 	top: 4px !important;" + 
		" 	width: 60% !important;" + 
		"   z-index: 255 !important;" +
		"}" + 
		".progress-label {" + 
		"	position: absolute !important;" + 
		"	left: 5% !important;" + 
		"	top: 4px !important;" + 
		"	font-weight: bold !important;" + 
		"	text-shadow: 1px 1px 0 #fff !important;" + 
		"   z-index: 256 !important;" +
		"}";
		
	// Debugging
	function debuglog(msg) {
		if (DO_DEBUG) {
			console.log(msg);
		}
	}
	
	function debugLogLvl(lvl, msg) {
		if (lvl & DEBUG_LVL_ERROR) {
			console.log("error: " + msg);
		} 
		if (DO_DEBUG) {
			if (lvl & DEBUG_LVL_WARN) {
				console.log("warn:" + msg);
			} 
			if (lvl & DEBUG_LVL_INFO) {
				console.log(msg);
			} 
		}
	}

	var g_index;
	var g_keys;
	var g_lengthOfKeysArray;
	var g_workInProgress = false;
	var g_par1 = UNDEF;
	var g_par2 = UNDEF;
	var g_workerFctDefault = function(key, par1, par2) {
		GM_deleteValue(key);
	};
	var g_workerFct = g_workerFctDefault;
	var g_finishFct_Default = function() {
		document.location.reload(true);
	};
	var g_finishFct = g_finishFct_Default;
	var g_label;

	function appendProgressBar() {
		$("body").append ( '\
			<div id="progressbar" class="ui-progressbar ui-progressbar-indeterminate"><div class="progress-label">History of the Seen: Resetting DB for current domain...</div></div>');
	}

	function removeProgressBar(reload) {
		$("#progressbar").remove();
		if (reload) {
			document.location.reload(true);
		}
	}
	
	/*
	 * Init function for "threading"
	 */
	function initThreadingLoop()
	{
		if (g_workInProgress) {
			debugLogLvl(DEBUG_LVL_ERROR, "initThreading with already threading in progress.");
			return;
		}
		
		g_workInProgress = true;
		g_index = 0;
		g_keys = GM_listValues();

		if (!g_keys) {
			debugLogLvl(DEBUG_LVL_WARN, "g_keys empty?"); 
			return;
		}

		g_lengthOfKeysArray = g_keys.length;
		appendProgressBar();
		
		progressbar = $("#progressbar");
		progressLabel = $(".progress-label");

		progressbar.progressbar({
			value : false,
			change : function() {
				progressLabel.text(g_label + progressbar.progressbar("value").toFixed(2) + "% ");
			},
			complete : function() {
				progressLabel.text(" History of the Seen: Operation Complete! ");
			}
		});

		progressbar.progressbar("value", 0);
		setTimeout(doThreadWork, 1);
	}

	/*
	 * Worker method
	 */
	function doThreadWork()
	{
		if (!g_workInProgress) {
			return;
		}
		
		var i = 0;
		var currentKey = null;
		
		currentKey = g_keys[g_index];
		while (i < defaultSettings.dbOpsPerRun && currentKey) {
			g_workerFct(currentKey, g_par1, g_par2);
			
			g_index ++;
			i++;
			currentKey = g_keys[g_index];
		}
		progressbar.progressbar("value", g_index * 100 / g_lengthOfKeysArray);
		if (currentKey) {
			setTimeout(doThreadWork, 10);
		} else
		{
			removeProgressBar(false);
			if (g_finishFct !== UNDEF) {
				g_finishFct();
			}
			g_workInProgress = false;
		}
	}
	
	
	// Resetting section
	function resetAllUrls() {
		if (!g_workInProgress && confirm('Are you sure you want to erase the complete seen history?')) {
			g_label = " History of the Seen: Cleaning DB, done ";
			g_par1 = UNDEF;
			g_par2 = UNDEF;
			g_workerFct = g_workerFctDefault;
			g_finishFct = g_finishFct_Default;
			initThreadingLoop();
		}
	}

	function resetUrlsForCurrentHelper(dKey, domainOrUri) {
		if (confirm('Are you sure you want to erase the seen history for ' + 
				domainOrUri + '?')) {
			g_label = " History of the Seen: Cleaning DB, done ";
			g_par1 = dKey;
			g_par2 = domainOrUri;
			g_workerFct = function(key, dKey, domainOrUri) {
				if (key == KEY_LAST_EXPIRE_OP){
					return;
				}
				try {
					var val = GM_getValue(key, "{}");
					var dict = JSON.parse(val);
					if(dict) {
						if (dict[dKey] == domainOrUri) {
							GM_deleteValue(key);
						}
					}
				} catch (e) {
					console.log(e);
				}
			};
			g_finishFct = g_finishFct_Default;
			initThreadingLoop();
		}
	}
	
	function resetUrlsForCurrentDomain() {
		resetUrlsForCurrentHelper("domain", document.domain);
	}
	
	function resetUrlsForCurrentSite() {
		resetUrlsForCurrentHelper("base", document.baseURI);
	}

	function expireUrls()	{
		debugLogLvl(DEBUG_LVL_INFO, "expireUrls");
		if (defaultSettings.cleanOnlyDaily) {
			var lastExpireDate = new Date(GM_getValue(KEY_LAST_EXPIRE_OP, nDaysOlderFromNow(2)));
			var diff = Math.abs((new Date()) - lastExpireDate);
			if (diff / 1000 / 3600 / 24 < 1) {
				// less than one day -> no DB cleaning
				return;
			}
		}

		/*
		var val = GM_getValue(KEY_EXPIRE_OP_INPROGRESS);

		if (typeof val !== UNDEF) {
			// expire in progress
			return;
		}
		GM_setValue(KEY_EXPIRE_OP_INPROGRESS, True);

		 */
		// cutOffDate
		g_label = " History of the Seen: Expiring old URLs for this site, done ";
		g_par1 = nDaysOlderFromNow(defaultSettings.ageOfUrl);
		debuglog("cutOffDate" + g_par1);
		g_par2 = UNDEF;
		g_workerFct = function(key, cutOffDate, par2) {
			if (key == KEY_LAST_EXPIRE_OP){
				return;
			}
			
			var dict = JSON.parse(GM_getValue(key, "{}"));
			if(dict) {
				try {
					debuglog(dict["domain"], cutOffDate.getTime(), dict["date"]);
					if (cutOffDate.getTime() > dict["date"]) {
						if (defaultSettings.expireAllDomains ||
							(dict["domain"] == document.domain))
						{
							GM_deleteValue(key);
						}
					}
				} catch (e) {
					console.log(e);
				}
			}
			else {
				console.log('Error! JSON.parse failed - dict is likely to be corrupted. Probably best to completely clean DB.');
			}
		};
		g_finishFct = function() {
			GM_setValue(KEY_LAST_EXPIRE_OP, new Date());
		}
		initThreadingLoop();
	}

	function nDaysOlderFromNow(age, aDate, zeroHour) {
		var aDate = typeof aDate !== UNDEF ? aDate : new Date();
		var zeroHour = typeof zeroHour !== UNDEF ? zeroHour : true;
		
		var dateStore = new Date(aDate.getTime());
		var workDate = aDate;
		if (age >= 0) {
			workDate.setDate(dateStore.getDate() - age);
			if (zeroHour) {
				workDate.setHours(0,0,0,0);
			}
		}
		return workDate;
	}

	/*
	 * Find the settings for a given URL
	 */
	function findPerUrlSettings(theSettings, aDomain) {
		debugLogLvl(DEBUG_LVL_INFO, "findPerUrlSettings");
		
		for (var i=0; i < theSettings.length; ++i) {
			for (var j = 0; j < theSettings[i].url.length; ++j) {
				var myRegExp = new RegExp(theSettings[i].url[j], 'i');
				if (aDomain.match(myRegExp)) {
					return theSettings[i];
				}
			}
		}
	}

	/*
	 * Find the parent element as specified in the settings.
	 */
	function locateParentElem(curSettings, aDomain, aRoot) {
		if (!curSettings) {
			return null;
		}
		// console.log("locateParentElem 1", curSettings.url);
		var res = null;
		for (var xpath = 0; xpath < curSettings.parentHints.length; ++xpath) {
			// console.log("locateParentElem 2", curSettings.parentHints[xpath], aRoot);
			res = document.evaluate(curSettings.parentHints[xpath], aRoot, null, 9, null).singleNodeValue;
			if (res) {
				// console.log("locateParentElem found something");
				return res;
			}
		}
		return res;
	}
	
	/*
	 * Check if the current node qualifies for looking up the hierarchy. 
	 */
	function goUp(curSettings, aRoot) {
		if (!curSettings) {
			return false;
		}

		var res = null;
		if (curSettings.upTrigger !== "") {
			res = document.evaluate(curSettings.upTrigger, aRoot, null, 9, null).singleNodeValue;
		}
		return res !== null;
	}
	
	
	/*
	 * Set the opacity for specified links
	 */
	function dimLinks() {
		var interval = (1 - defaultSettings.targetOpacity4Dim)/defaultSettings.steps;
		var countDownTimer = countDownTimer - 1;
		var curOpacity = defaultSettings.targetOpacity4Dim + interval*countDownTimer;

		// TODO: Better iterate over dimmap
		for(var i = 0; i < theHRefs.length; i++)
		{
			var hash = 'm' + hex_md5(theHRefs[i].href);
			if (hash in dimMap) {
				theHRefs[i].style.opacity = curOpacity;
			}
		}
		
		if (countDownTimer > 0) {
			var to = setTimeout(dimLinks, defaultSettings.dimInterval);
		}
	}
	
	/*
	 * Check if an element is fully drawn on the viewport 
	 * from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling?lq=1
	 */
	function isFullyInView(elem)
	{
		debugLogLvl(DEBUG_LVL_INFO, "isFullyInView");
	    var docViewTop = $(window).scrollTop();
	    var docViewBottom = docViewTop + $(window).height();

	    var elemTop = $(elem).offset().top;
	    var elemBottom = elemTop + $(elem).height();

	    return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));

		// is really fully in view    
		// return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom)
		//  && (elemBottom <= docViewBottom) &&  (elemTop >= docViewTop) );
	}

	/*
	 * Called after scrolling finished for defined time
	 */
	function evaluateElems() {
		debugLogLvl(DEBUG_LVL_INFO, "evaluate all");
		processElements(false);
		timeOutAfterLastScroll = UNDEF;
	} 
	
	/*
	 * Wait for scrolling to end
	 */
	function onScroll()
	{
		if (timeOutAfterLastScroll !== UNDEF) {
			window.clearTimeout(timeOutAfterLastScroll)
		}
		timeOutAfterLastScroll = setTimeout(evaluateElems, AFTER_SCROLL_DELAY);
	}

	/*
	 * Process all elements
	 */
	function processElements(firstCall) {
		debugLogLvl(DEBUG_LVL_INFO, "processElements");
		var allTagElems = document.getElementsByTagName(tag2Process);
		var elemMap = {};
		var theBase = document.baseURI;

		// Change the DOM

		// First loop: gather all new links and make already seen opaque.
		for(var i = 0; i < allTagElems.length; i++)
		{
			var hash = 'm' + hex_md5(getContentFct(allTagElems[i]));
			// setValue needs letter in the beginning, thus use of 'm'
			debugLogLvl(DEBUG_LVL_INFO, "hash: " + hash);

			var key = GM_getValue(hash);

			
			if (typeof key !== UNDEF && key !== null) {
				// workaround for issue https://github.com/greasemonkey/greasemonkey/issues/2156
				
				// key found -> loaded this reference already 
				debugLogLvl(DEBUG_LVL_INFO, "key found");

				if (firstCall) {
					var done = false;
					if(goUp(curSettings, allTagElems[i])) {
						var pe = locateParentElem(curSettings, theDomain, allTagElems[i])
						// console.log("locate parent done", pe);
						if (pe) {
							pe.style.opacity = defaultSettings.targetOpacity;
							done = true;
						}
					}
					if (!done) {
						// change display
						allTagElems[i].style.opacity = defaultSettings.targetOpacity;
						debugLogLvl(DEBUG_LVL_INFO, "changing opacity");
					}
				}
				
			} else {
				//check if element is fully visible
				debugLogLvl(DEBUG_LVL_INFO, "key not found");
				
				if (isFullyInView(allTagElems[i])) {
					debuglog(allTagElems[i] + " is in view");

					// key not found, store it with current date
					elemMap[hash] = {"domain":theDomain, "date":(new Date()).getTime(), "base":theBase};
					dimMap[hash] = allTagElems[i];
				}
			}
		}

		// remember all new urls to hide the next time
		for (var e2 in elemMap) {
			GM_setValue(e2, JSON.stringify(elemMap[e2]));
		}
		
		theHRefs = allTagElems;
		if (firstCall) {
			var to = setTimeout(dimLinks, defaultSettings.dimInterval);
		}
	}
	
//	Menus
	GM_registerMenuCommand("Remove the seen history for this site.", resetUrlsForCurrentSite);
	GM_registerMenuCommand("Remove the seen history for this domain.", resetUrlsForCurrentDomain);
	GM_registerMenuCommand("Remove all seen history (for all sites)!", resetAllUrls);

	GM_addStyle(progressBarStyle);

//	Main part
	function run_script() {
		debugLogLvl(DEBUG_LVL_INFO, "run");
		dimMap = {};
		theDomain = document.domain;

		curSettings = findPerUrlSettings(perUrlSettings, theDomain);
		tag2Process = DEFAULT_TAG;
		getContentFct = defaultGetContentFct;
		if (typeof curSettings != UNDEF) {
			if (typeof curSettings.tag != UNDEF) {
				tag2Process = curSettings.tag;
			}
			if (typeof curSettings.getContent != UNDEF) {
				getContentFct = curSettings.getContent;
			}
		}
		
		expireUrls();
		processElements(true);
		window.addEventListener("scroll", onScroll, false);
	}

	run_script();
 })();