OKCupid Threaded Favorites' comments

By zzyzx_xyzzy Last update May 16, 2008 — Installed 184 times. Daily Installs: 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install the script, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts,
// select "OKC Threaded Favorites' Comments", and click Uninstall.
//
// --------------------------------------------------------------------
// 
// Author's contact info:
//
// zzyzx.xyzzy@gmail.com
// zzyzx_xyzzy (OKCupid)
// zzyzx_xyzzy@userscripts.org
// http://zzyzx.xyzzy.googlepages.com/okcupidhacks
// 
// Copyright (c) 2008, the author.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the author nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

//
// ==UserScript==
// @name           OKC Threaded Favorites' comments
// @namespace      http://zzyzx.xyzzy.googlepages.com/okccommentsthreading
// @description    Organizes the favorites' comments screen by thread and allows you to expand and collapse threads.
// @include        http://*.okcupid.com/relevant*comm*
// ==/UserScript==

//find all the comments

var comments = document.evaluate(
	"//a[@class='jcomHomeTitleLink']"
+	"/ancestor::div[contains(concat(' ', @class, ' '), ' journalRelevantComment ')][1]",
	document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

if (comments.snapshotLength <= 0) return;

var commentlist = [];
for (var i = 0; i < comments.snapshotLength; i++) {
	commentlist.push(comments.snapshotItem(i));
}

//I want to sort them into threads, then sort the threads by the date of the most recent
//comment therein.
var threads = {};

for (var i = 0; i < commentlist.length; i++) {
	var next = commentlist[i];
	var threadid = thread(next);
    GM_log(threadid);
	if (typeof(threads[threadid]) == 'undefined') {
		threads[threadid] = [];
	}
	threads[threadid].push(next);
}

var sortedthreads = [];
for (var i in threads) {
	sortedthreads.push(threads[i]);
}

/*
var dates = map(function(t) {return parseInt(date(t[0]));}, sortedthreads);
GM_log(dates); // the dates to be sorted...

//this sorting doesn't work!!! Luckily it already appears to be sorted. I don't
//know if that will hold in the future... But seriously, wtf?
sortedthreads.sort(function(a, b) {strcmp(parseInt(date(b[0])), parseInt(date(a[0])))});

//Watch how sort doesn't work!
dates.sort(function(a, b) {strcmp(parseInt(b), parseInt(a))});
c = map(function(t) {return date(t[0]);}, sortedthreads);
GM_log(c); //LOOK HOW IT DOESN'T SORT THE ENTRIES
GM_log(dates); //AND ALSO LOOK HOW IT DOESN'T SORT STRINGS
*/

//now they should be sorted into threads, the threads sorted reverse
//chronologically by latest comment in thread, assuming they were reverse
//chronological to begin with.

//insert a silly little marker before, to insert by.
var marker = commentlist[0].parentNode.insertBefore(e('span', {id:'threadingMarker'}, []), commentlist[0]);

for (var threadix = 0; threadix < sortedthreads.length; threadix++) {
	var thread = sortedthreads[threadix];
	//push in a new thread heading
	marker.parentNode.insertBefore(makePostWrapper(thread), marker);
}

function makePostWrapper(thread) {
	//hee hee, needed a function to separate scopes of collapseLink and expandLink

	//are ALL the comments collapsed already?
	//Show up in collapsed mode if so.

	var allCollapsed = true;
	for (var i = 0; i < thread.length; i++) {
		if (!(thread[i].getAttribute('class').match('collapsed'))) {
			allCollapsed = false;
		}
	}

	var collapseLink = e('a', {href:'javascript:void(0)'}, [t('hide')]);
	collapseLink.addEventListener('click', function(evt) {
		postDiv.style.display='none';
		collapseLink.parentNode.replaceChild(expandLink, collapseLink);
	}, false);

	var expandLink = e('a', {href:'javascript:void(0)'}, [t('show')]);
	expandLink.addEventListener('click', function(evt) {
		postDiv.style.display='';
		expandLink.parentNode.replaceChild(collapseLink, expandLink);
	}, false);	

	var postDiv = e('div', {style:(allCollapsed ? 'display:none;' : '')}, thread);			
	var postWrapper = e('div', {class:'text', style:''}, [
		e('h3', {style:"font-size: 10pt;"}, [
			t('' + thread.length + ' comment' + (thread.length == 1 ? '' : 's') + ' on '),
			postlink(thread[0]),
			t(' ('),
			(allCollapsed ? expandLink : collapseLink),
			t(')'),
		]),
		postDiv,
	]);
	return postWrapper;
}


//---- scrapers -----

function thread(post) {
	//scrape a thread identifier from a comment.
	var result =  document.evaluate(
		"descendant::a[@class='jcomHomeTitleLink']/@href",
		post, null, XPathResult.STRING_TYPE, null).stringValue;
    result = result.match(/\/journal\/(\d+)/) || result.match(/pid=(\d+)/);
    if (result) {
        result = result[1];
    }
    GM_log('_' + result);
	return result;
}

function date(post) {
	//scrape a date number from a comment. If no date found, then return 0.
	var value = document.evaluate("number(substring-before(substring-after(substring-after(descendant::script[1],'Date' ), '('),' '))",
		post, null, XPathResult.NUMBER_TYPE, null).numberValue;
	if (isNaN(value)) {
		value = 0;
	}
	return value;
}

function postlink(post) {
	//clone the post link from a post.
	return document.evaluate(
		"descendant::a[@class='jcomHomeTitleLink']",
		post, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
	).singleNodeValue.cloneNode(true);
}

//------ helpers ------

function uniq(list, cmp) {
	//Takes an optional ordering which is used for comparison and equality
	cmp = (typeof(cmp) != 'undefined') ? cmp : strcmp;
	sorted = list.sort(cmp);
	var b = [];
	if (sorted.length) {
		b.push(sorted.shift())
	}
	while (sorted.length) {
		var next = sorted.shift()
		if (cmp(next, b[b.length - 1])) {
			b.push(next);
		}
	}
	return b;

}

function strcmp(a, b) {
	if (a > b) {
		//GM_log(a + ' > ' + b);
		return 1;
	}
	if (a < b) {
		//GM_log(a + ' < ' + b);
		return -1;
	}
	//GM_log(a + ' = ' + b);
	return 0;
}


function map(fn, a) {
	var b = [];
	for (i = 0; i < a.length; i++) {
		b.push(fn(a[i]));
	}
	return b;
}

function e(name, attribs, children) {
	//make an element with some attributes and children.
	var r = document.createElement(name);
	for (var i in attribs) {
		r.setAttribute(i, attribs[i]);
	}
	for (var i = 0; i < children.length; i++) {
		r.appendChild(children[i]);
	} 
	return r;
}

function t(text) {
	//make a text node.
	return document.createTextNode(text);
}