There are 17 previous versions of this script.
// ==UserScript==
// @name Registered Commenter Filter for Slog and Line Out
// @description Filters commenters and nests replies
// @icon https://github.com/jonathancollins/the-stranger-greasemonkey/raw/a0fd7337551e5d843a9af2e8d9c28776183c8283/icon32.png
// @version 2.0.1
// @author Jonathan Collins
// @copyright 2011 Jonathan Collins
// @attribution Commenter filter idea and prototype by Dennis and Katrin Bratland
// @namespace http://joncollins.name/
// @include http://slog.thestranger.com/*/archives/*
// @include http://lineout.thestranger.com/*/archives/*
// @include http://www.thestranger.com/*/archives/*
// @include http://www.thestranger.com/seattle/Comments?*
// @include http://www.thestranger.com/seattle/*/Content?*
// @include http://www.thestranger.com/seattle/SavageLove?*
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js
// @require http://sizzlemctwizzle.com/updater.php?id=48588
// ==/UserScript==
// Version 2.0.1
//
// * Fixed nesting not working with more than 2 comments referenced on a single @/#
// * Added "SHARE VIA" URL
//
// Version 2.0.0
//
// * Added option to enable/disable comment nesting using #N and @N references
//
// * Added option to enable/disable comment hiding
//
// * Hidden commenters stay hidden after using the "Registered" toggle button
//
// * Support for online features and Savage Love
//
// Version 1.1.2
//
// * Clicking 'hide' or unchecking a box only acts on that user's comments (it was
// hiding comments that had been opened by clicking a collapsed bar)
//
// * No longer interferes with the Stranger's built-in Registered/Unregistered
// filters
//
// * Hide link is inserted after user website icon, if it exists
//
// Version 1.1.1
//
// * Added link to a google search on each filtered commenter
//
// Version 1.1.0
//
// * Utilized the built-in collapse controls to allow the user to show
// a filtered comment
//
// * Included sizzlemctwizzle's auto update script
//
// Version 1.0.2
//
// * Added support for Line Out
//
// Version 1.0.1
//
// * Fixed "hide" link appearing next to anonymous comments. This might be
// a useful feature in the anonymous comment script, but I left it out of
// this one because anonymous commenter names can be sock-puppeted, etc.
// This script is supposed to deal with registered commenters.
//
// Version 1.0.0
//
// * Initial release
$(document).ready(function()
{
userOptions = getUserOptions();
injectOptions();
if (userOptions['nest']) {
//check to make sure comments are in expected structure?
var valid = true;
//abort if not (could seriously break the page)
if (valid)
{
nestComments();
}
}
if (userOptions['hide']) {
hideComments();
injectAuthorCheckboxes();
injectHideLinks();
}
});
/****************************************************
* *
* Comment nesting *
* *
****************************************************/
function nestComments()
{
var comments = {};
var max = 0;
var nests = {};
$(".comment:not(.collapsed) .commentNumber").each(function(i, e)
{
//create a map of comment number to comment
var n = parseInt(e.innerHTML);
comments[n] = $(e).parent().parent();
//get the highest comment number
//for reverse iteration
max = Math.max(max, n);
//create a reply nest for each comment
var nest = $(document.createElement('div'))
.attr('id', comments[n].attr('id') + '-nest')
.css('margin-left', '40px')
.css('min-width', '260px')
.addClass('nest')
.insertAfter(comments[n]);
nests[n] = nest;
});
//match replies in "#N" or "@N" format
//also matches variants of "@N and M"
var replyRegExp = /[#@] ?([0-9]+(?: ?(?:,|\/|and|&) ?[0-9]+)*)/g;
var splitRegExp = / ?(?:,|\/|and|&) ?/;
//loop through comments in reverse order
for (var n = max; n > 0; n--)
{
//handle missing comments
if (comments[n] == undefined)
{
continue;
}
var commentBody = $(".commentBody", comments[n])
var parents = [];
var match;
//search for @N or #N references
while ((match = replyRegExp.exec(commentBody.html())) != null)
{
//account for multiple references in one @/#
var matches = match[1].split(splitRegExp);
for (var i in matches) {
var inReplyTo = parseInt(matches[i]);
//avoid false positives and circular references
if (inReplyTo < n && comments[inReplyTo] != undefined)
{
//index by inReplyTo to avoid duplicating replies
parents[inReplyTo] = inReplyTo;
}
}
}
//nest the comment
if (parents.length > 0)
{
for (p in parents)
{
//clone comment's nest
var nestClone = nests[n].clone()
.attr('id', parents[p] + '-' + nests[n].attr('id')); //assign new id
//reassign ids inside nest
$('.comment, .nest', nestClone).each(
function(i) {
$(this).attr('id', parents[p] + '-' + $(this).attr('id'));
}
);
//clone comment
var commentClone = comments[n].clone()
.attr('id', parents[p] + '-' + comments[n].attr('id')); //assign new id
//clone collapsed comment
var collapsed = $('#' + comments[n].attr('id') + '-collapsed');
var collapsedClone = collapsed.clone()
.attr('id', p + '-' + comments[n].attr('id') + '-collapsed'); //assign new id
//make sure the expander acts on the correct comment
var expander = $('a:first', collapsedClone);
//using .attr('onclick', ...) or .removeAttr('onclick') is causing some
//kind of conflict in a greasemonkey sandbox, so use the raw DOM
expander.get(0).setAttribute('onclick', '');
//using jQuery events simply doesn't work after the cloning process
/*
expander.click(function expand(event) {
$(event.target).parent().hide();
$(event.target).parent().next().fadeIn(0.5);
});
*/
//instead use The Stranger's prototype install with an onclick attribute
expander.get(0).setAttribute('onclick', '$(this).up().hide();$(this).up().next().appear({duration:0.5});');
//prepend everying to the parent's reply nest
nests[p]
.prepend(nestClone)
.prepend(commentClone)
.prepend(collapsedClone);
}
}
}
//nesting complete
//collapse all but the first instance of a comment
var appeared = {};
$(".comment:not(.collapsed) .commentNumber").each(function(i, e)
{
var n = parseInt(e.innerHTML);
if (appeared[n] == undefined) {
appeared[n] = true;
}
else {
var comment = $(e).parent().parent();
var commentId = comment.attr('id');
var nestId = commentId + '-nest';
var nest = $('#' + nestId);
var collapsedId = commentId + '-collapsed';
var collapsed = $('#' + collapsedId);
//collapse comment in place
comment.hide();
nest.hide();
//and give a message as to why it's collapsed
collapsed
.append($(document.createElement('p'))
.css('text-align', 'left')
.append($(document.createElement('small'))
.html('The comment above is nested elsewhere')));
collapsed.show();
}
});
}
/*
var message = $(document.createElement('small'))
.html('The above comment is nested below comment'
+ (parents.length > 1 ? '(s)' : ''));
for (p in parents)
{
var anchor = $('a:first', comments[parents[p]]).attr('name');
message
.append(document.createTextNode(' '))
.append($(document.createElement('a'))
.attr('href', '#' + anchor)
.html(parents[p])
);
}
collapsed.append($(document.createElement('p')).append(message));
*/
/****************************************************
* *
* Comment hiding *
* *
****************************************************/
var authorStatus = getAuthorStatus();
function hideComments() {
if ($("#BrowseComments").size() > 0) {
$(".commentByline .commentAuthor").each(
function(i) {
// get the id of the comment div
// also for use showing the collapse control
var commentId = $(this).parent().parent().attr('id');
var collapseId = commentId + '-collapsed';
// there is no need to check anonymity here
// anonymous comments cannot sock puppet registered names
var author = $(this).html();
// hide the comment and show collapse control if author is set to invisible
if (authorStatus[author] !== undefined) {
$('#' + commentId).hide();
$('#' + collapseId).show();
}
}
);
}
}
function setAuthorVisibility(author, visible) {
if ($("#BrowseComments").size() > 0) {
$(".commentByline .commentAuthor").each(
function(i) {
var currentAuthor = $(this).html();
if (currentAuthor == author) {
var comment = $(this).parent().parent();
//don't expand nested comments
if (comment.hasClass('nested')) {
return;
}
// get the id of the comment div
// also for use showing the collapse control
var commentId = comment.attr('id');
var collapseId = commentId + '-collapsed';
// hide or show the comment and collapse control
if (visible === false) {
$('#' + commentId).hide();
$('#' + collapseId).show();
}
else {
$('#' + commentId).show();
$('#' + collapseId).hide();
}
}
}
);
}
}
/****************************************************
* *
* UI injection *
* *
****************************************************/
function injectSidebarDiv(div) {
// append updated list to right sidebar
var sidebar = $('#gridSpanningIsland');
if (sidebar.size() > 0) {
div.css('width', '330px');
div.appendTo($('#gridSpanningIsland'));
}
// it's called something different on comment popups
var sidebar = $('#gridRightSidebar');
if (sidebar.size() > 0) {
div.css('width', '280px');
div.appendTo($('#gridRightSidebar'));
}
// and something else in online articles and Savage Love
var sidebar = $('#mainRight');
if (sidebar.size() > 0) {
div.css('width', '330px');
div.appendTo($('#mainRight'));
}
}
function injectAuthorCheckboxes() {
// create a sorted author list array
var authors = new Array();
for (var author in authorStatus) {
authors.push(author);
}
if ($('#BrowseComments').size() > 0) {
if (authors.length == 0) {
//try to remove existing list
$('#HiddenCommenters').remove();
return;
}
//sort case insensitive
authors.sort(function(x, y) {
var a = x.toUpperCase();
var b = y.toUpperCase();
if (a > b) {
return 1;
}
if (a < b) {
return -1;
}
return 0;
});
// create the checkboxes and surrounding div
var div = $(document.createElement('div'))
.css('background', '#FFFFFF none repeat scroll 0 0')
.css('float', 'left')
.css('margin', '10px 0')
.css('padding', '10px')
.css('text-align', 'left');
$(document.createElement('h2'))
.addClass('sitesection')
.text('Hidden Commenters')
.appendTo(div);
for (var i = 0; i < authors.length; i++) {
getAuthorCheckbox(authors[i]).appendTo(div);
}
// remove existing list
$('#HiddenCommenters').remove();
div.attr('id', 'HiddenCommenters');
injectSidebarDiv(div);
}
}
function getAuthorCheckbox(author) {
var div = $(document.createElement('div'));
var input = $(document.createElement('input'))
.attr('type', 'checkbox')
.attr('value', author)
.attr('checked', 'checked')
.bind('change', function(e) {
if (this.checked == true) {
// this shouldn't actually happen...
authorStatus[this.value] = true;
setAuthorVisibility(this.value, false);
}
else {
delete authorStatus[this.value];
setAuthorVisibility(this.value, true);
// ...because of this
injectAuthorCheckboxes();
}
saveAuthorStatus();
})
//checkbox
input.appendTo(div);
//space
$(document.createTextNode(' ')).appendTo(div);
//link
var a = $(document.createElement('a'))
.attr('href', 'http://www.google.com/search?q=%22' + author.replace('/ /', '%20') + '%22%20site%3A' + document.domain + '&tbo=s&tbs=qdr:y,sbd:1')
.attr('target', '_blank');
$(document.createTextNode(author)).appendTo(a);
a.appendTo(div);
return div;
}
function injectHideLinks() {
if ($("#BrowseComments").size() > 0) {
$(".commentByline .commentAuthor").each(
function(i) {
//skip adding "hide" link next to anonymous comments
if ($(this).hasClass('anonymous')) return;
var author = $(this).html();
var span = $(document.createElement('span'));
var a = $(document.createElement('a'))
.text('hide')
.bind('click', function(e) {
authorStatus[author] = true;
setAuthorVisibility(author, false);
injectAuthorCheckboxes();
saveAuthorStatus();
})
$(document.createTextNode(' [')).appendTo(span);
a.appendTo(span);
$(document.createTextNode(']')).appendTo(span);
$('.commentDate', $(this).parent()).before(span);
}
);
}
}
function injectOptions() {
// create the checkboxes and surrounding div
var div = $(document.createElement('div'))
.css('background', '#FFFFFF none repeat scroll 0 0')
.css('float', 'left')
.css('margin', '10px 0')
.css('padding', '10px')
.css('text-align', 'left');
$(document.createElement('h2'))
.addClass('sitesection')
.text('Options')
.appendTo(div);
hideOption = getOptionCheckbox('hide', 'Hide commenters', function(e) {
userOptions['hide'] = this.checked;
saveUserOptions();
$('#refreshDiv').show();
});
hideOption.appendTo(div);
nestOption = getOptionCheckbox('nest', 'Nest comments', function(e) {
userOptions['nest'] = this.checked;
saveUserOptions();
$('#refreshDiv').show();
});
nestOption.appendTo(div);
refreshDiv = $(document.createElement('div'))
.attr('id', 'refreshDiv')
.css('margin-top', '10px')
.css('display', 'none');
refreshButton = $(document.createElement('button'))
.html('Refresh')
.bind('click', function(e) { location.reload(true) })
.appendTo(refreshDiv);
refreshDiv.appendTo(div);
injectSidebarDiv(div);
}
function getOptionCheckbox(name, label, event) {
var option = $(document.createElement('div'));
var checkbox = $(document.createElement('input'))
.attr('type', 'checkbox')
.attr('name', name + 'Option');
if (userOptions[name] == true) {
checkbox.attr('checked', 'checked');
}
checkbox.bind('change', event);
checkbox.appendTo(option);
$(document.createTextNode(' ')).appendTo(option);
$(document.createElement('label'))
.attr('for', name + 'Option')
.html(label)
.appendTo(option);
return option;
}
/****************************************************
* *
* Storage *
* *
****************************************************/
function saveUserOptions() {
GM_setValue('userOptions', userOptions.toSource());
}
function getUserOptions() {
var userOptions = GM_getValue('userOptions');
if (userOptions === undefined) {
userOptions = {
hide: true,
nest: true
};
}
else {
userOptions = eval(userOptions);
}
return userOptions;
}
function saveAuthorStatus() {
GM_setValue('authorStatus', authorStatus.toSource());
}
function getAuthorStatus() {
var authorStatus = GM_getValue('authorStatus');
if (authorStatus === undefined) {
authorStatus = new Object();
}
else {
authorStatus = eval(authorStatus);
}
return authorStatus;
}