User:Colin M/gadgets/hollis debug.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
"use strict"; // <nowiki>
/*jshint shadow:true, scripturl:true, loopfunc:true, latedef:true, undef:true */
/*global mw, jQuery */
/* 
TODOS:
- set wllimit appropriately. Ideally should approximately match page limit for current watchlist view? (assuming filtering is on point - see bullet above)
- figure out what "Expand watchlist to show all changes, not just the most recent" does, and whether it should truly block script from running.
- Use of "wgCanonicalSpecialPageName" is deprecated. Use mw.config instead.
- mark original @ User:Kephir/gadgets/hollis as broken/deprecated?
- sometimes the 'since last seen' link is the same as the normal 'n changes' diff link. 
	- Not clear to me whether this is an actual bug. If it is, should try to fix it.
	- If it's expected behaviour... should still try to fix it.

cf. https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.util
also https://www.mediawiki.org/wiki/API:Setnotificationtimestamp <- this is the thing to look at for the 'mark as read' functionality
*/
(function(){
function el(tag, child, attr, events) {
	var node = document.createElement(tag);

	if (child) {
		if (typeof child !== 'object')
			child = [child];
		for (var i = 0; i < child.length; ++i) {
			var ch = child[i];
			if ((ch === void(null)) || (ch === null) || (ch === false))
				continue;
			else if (typeof ch !== 'object')
				ch = document.createTextNode(String(ch));
			node.appendChild(ch);
		}
	}

	if (attr) for (var key in attr) {
		node.setAttribute(key, String(attr[key]));
	}

	if (events) for (var key in events) {
		node.addEventListener(key, events[key], false);
	}

	return node;
}

if ((window.wgCanonicalSpecialPageName === "Watchlist") && (true || !mw.user.options.get('extendwatchlist')) && !window.kephirHollisLeaveWatchlistAlone)
mw.loader.using(['mediawiki.api', 'mediawiki.user', 'mediawiki.Title', 'mediawiki.Uri'], function () {
	console.log('Passed check');

var api = new mw.Api();
var chlist = document.getElementsByClassName('mw-changeslist')[0];
// Spans containing the titles of watchlist entries
var titles = chlist.getElementsByClassName('mw-title');
// Mapping from diff revision id to <a> eles?
var titlemap = {};

var hideopts = {
	bot: Number(mw.util.getParamValue('hidebots')      || mw.user.options.get('watchlisthidebots')),
	ano: Number(mw.util.getParamValue('hideanons')     || mw.user.options.get('watchlisthideanons')),
	// logged-in users
	liu: Number(mw.util.getParamValue('hideliu')       || mw.user.options.get('watchlisthideliu')),
	own: Number(mw.util.getParamValue('hidemyself')       || mw.user.options.get('watchlisthideown')),
	// ???
	pat: Number(mw.util.getParamValue('hidePatrolled') || mw.user.options.get('watchlisthidepatrolled')),
	min: Number(mw.util.getParamValue('hideminor')     || mw.user.options.get('watchlisthideminor')),
};

for (var i = 0; i < titles.length; ++i) {
	// <a> elements on this watchlist line. (NB: we don't count collapsed entries for individual diffs when there are multiple changes)
	// page, diff, history, user links
	var links = titles[i].parentNode.getElementsByTagName('a');
	for (var j = 0; j < links.length; ++j) {
		var href = new mw.Uri(links[j].href);
		// diff urls have query params: curid, oldid, diff. (Semantics?)
		// in particular, what is curid? Seems to generally be much smaller than oldid and diff. Maybe curr page id? Yup, confirmed.
		// so diff is the revid of the latest change
		if (href.query.oldid && href.query.diff) {
			titlemap[String(parseInt(href.query.diff, 10))] = links[j];
			break;
		}
	}
}

// make a diff link
function makeLink(title, pageid, revid, oldid) {
	return mw.util.getUrl(title, { "curid": pageid, "diff": revid, "oldid": oldid });
}

function prepareLink(link, ready) {
	if (!/^javascript/.test(link.href))
		return;
	if (link.busy)
		return;
	link.busy = true;

	// cf. https://www.mediawiki.org/w/api.php?action=help&modules=query%2Brevisions
	// So I guess the idea here is to get the single revision id that comes immediately before
	// link.pageinfo.notificationtimestamp? Wait, but why rvlimit=2? Maybe that timestamp
	// is the exact timestamp of a revision, and so we want the revision immediately before
	// that one?
	api.get({
		action: 'query',
		pageids: link.pageinfo.pageid,
		prop: 'revisions',
		rvprop: 'ids',
		rvstart: link.pageinfo.notificationtimestamp,
		// rvstartid: link.pageinfo.old_revid,
		rvdir: 'older',
		rvlimit: 2
	}, {
		success: function (result) {
			if (result.error) {
				link.href = 'javascript:void("API_ERROR_WHILE_ACQUIRING_LINK:CODE=' + result.error.code + '");';
				link.busy = false;
				console.error(result.error);
				return;	
			}
			var revs = result.query.pages[link.pageinfo.pageid].revisions;
			link.href = makeLink(link.pageinfo.title, link.pageinfo.pageid, link.pageinfo.revid, (revs[1] || revs[0]).revid);
		},
		error: function () {
			link.href = 'javascript:void("ERROR_WHILE_ACQUIRING_LINK");';
			link.style.opacity = '0.5';
			link.busy = false;
			console.error(arguments);
		}
	});
}

var havePatrol = false;

function grabWatchlist(cont) {
	// This seems to be limited to 10 results? Seems pretty limiting. so to speak. wllimit default = 10...
	// cf. https://www.mediawiki.org/wiki/API:Watchlist
	var query = {
		action: 'query',
		list: 'watchlist',
		/* notificationtimestamp: "timestamp of when the user was last notified about the edit."
		 If field is null, we treat that as 'seen it' (which is kinda curious).
		 in prepareLink() (i.e. where we set up the logic for the 'since last seen' links), 
		 we do an API query for the revision immediately before notificationtimestamp. Then the 'since last seen'
		 link goes to a diff starting from that queried revision going up until link.pageinfo.revid (i.e. item.revid)
		 So I guess it's the timestamp of the last revision of this page seen by the user? 
		 timestamp: "timestamp of the edit"
		*/
		wlprop: 'ids|title|timestamp|notificationtimestamp' + (havePatrol ? '|patrol' : ''), // why does it have to error? could the API not just ignore wlprop=patrol?
		wltype: 'edit',
		wllimit: 100,
		// "Show only items that meet these criteria." (catted below)
		wlshow: ''
	};

	if (hideopts.own) query.wlexcludeuser = mw.config.get('wgUserName');
	if (hideopts.bot) query.wlshow += '|!bot';
	if (hideopts.ano) query.wlshow += '|!anon';
	if (hideopts.liu) query.wlshow += '|anon';
	if (hideopts.min) query.wlshow += '|!minor';

	if (cont) {
		for (var key in cont) {
			query[key] = cont[key];
		}
	}

	api.get(query, {
		success: function (result) {
			for (var i = 0; i < result.query.watchlist.length; ++i) {
				var item = result.query.watchlist[i];
				// If this page is not in titlemap (i.e. not one of the pages listed on the current watchlist page)
				if (!titlemap[String(item.revid)])
					// ??? 'patrol' seems to be an annoyingly overloaded term.
					/* Oh, I think I see what's going on here. Hideopts is supposed to represent all the currently active watchlist
					filters. We try to bake them into the API query so that what we get back ought to match what's currently on the page.
					The exception seems to be this patrol thing (whatever it is) which is post-filtered here. 
					
					For some reason, hideopts is all zeros for me. Maybe old vs. new watchlist thing?
					*/
					if (hideopts.pat && ('patrolled' in item))
						continue;
					else
						continue;
						//return; // we seem to have reached the end.
				if (!item.notificationtimestamp) // seen it. skip.
					continue;

				var seenlink;
				if (item.notificationtimestamp !== item.timestamp) {
					seenlink = el('a', ['since last seen'], {
						"href": 'javascript:void("PLEASE_WAIT_AND_TRY_AGAIN");'
					}, {
						"mouseover": function () {
							prepareLink(this);
						},
						"mousedown": function () {
							prepareLink(this);
						},
						"click": function () {
							prepareLink(this, function (href) {
								location.href = href;
							});
						}
					});
					seenlink.pageinfo = item;
				} else {
					if (!window.kephirHollisShowRedundant)
						continue;
					seenlink = el('a', ['since last seen'], {
						"href": makeLink(item.title, item.pageid, item.revid, item.oldid)
					});
				}

				var difflink = titlemap[String(item.revid)];
				difflink.parentNode.insertBefore(seenlink, difflink);
				difflink.parentNode.insertBefore(document.createTextNode('\xa0| '), difflink);
			}
			
			if (!('batchcomplete' in result))
				grabWatchlist(result["continue"]);
		}
	});
}

if (hideopts.pat)
	mw.user.getRights(function (rights) {
		havePatrol = (rights.indexOf("patrol") !== -1);
		grabWatchlist();
	});
else
	grabWatchlist();

});
/* I have no idea what this second part is about. Active on diff pages. 
Seems to add rev delete buttons in diff view? Seems like an bed fellow of the watchlist stuff.
Okay, https://en.wikipedia.org/wiki/User:Kephir/gadgets/hollis says...
	"When viewing diffs spanning several revisions of a page, you can click a link to view a list of intermediate revisions."
*/
else if (mw.util.getParamValue('diff') && mw.util.getParamValue('diff') && document.getElementsByClassName('diff-multi').length && !window.kephirHollisLeaveDiffsAlone)
mw.loader.using(['mediawiki.api', 'mediawiki.user', 'mediawiki.Title', 'mediawiki.Uri', 'mediawiki.language.months'], function () {

var haveRevdel = false;
var haveBlock  = false;

function fmtDate(date) {
	// XXX: uses browser's time zone instead of preferences
	date = new Date(date);
 
	switch (mw.user.options.get('date')) {
	case 'dmy'     : return date.toLocaleTimeString() + ', ' + date.getDate() + ' ' + mw.language.months.genitive[date.getMonth()] + ' ' + date.getFullYear();
	case 'mdy'     : return date.toLocaleTimeString() + ', ' + mw.language.months.genitive[date.getMonth()] + ' ' + date.getFullYear() + ', ' + date.getDate();
	case 'ymd'     : return date.toLocaleTimeString() + ', ' + date.getFullYear() + ' ' + mw.language.months.genitive[date.getMonth()] + ' ' + date.getDate();
	case 'default' : return date.toLocaleString();
	case 'ISO 8601': return date.toISOString(); 
	}
}

var api = new mw.Api();

var linkwrap, linklabel, revsform;
var dm = document.getElementsByClassName('diff-multi')[0]; // XXX: better detection
dm.appendChild(linkwrap = el('span', [
	'\xa0[',
	el('a', [linklabel = document.createTextNode('show list')], {
		"href": 'javascript:void(window.warranty);'
	}, {
		"click": function (ev) {
			if (!revsform)
			mw.user.getRights(function (rights) {
				console.log("Got rights, ", rights);
				haveRevdel = (rights.indexOf("deleterevision") !== -1);
				haveBlock  = (rights.indexOf("block") !== -1);

				var lower = Number(mw.util.getParamValue('oldid'));
				var hiher = Number(mw.util.getParamValue('diff'));
			
				if (hiher < lower) {
					var t = lower;
					lower = hiher;
					hiher = t;
				}
			
				var revstab;
				revsform = el('form', [
					el('input', null, { type: "submit", value: "View difference" }),
					' ',
					haveRevdel && el('button', ["Change visibility"], { type: "submit", name: "action", value: "revisiondelete" }),
					revstab = el('table', null, {
						"class": "wikitable", // XXX
						"style": "text-align: left; margin: 0.5em auto;"
					}),
					el('input', null, { type: "submit", value: "View difference" }),
					' ',
					haveRevdel && el('button', ["Change visibility"], { type: "submit", name: "action", value: "revisiondelete" }),
				], { action: mw.config.get('wgScript'), style: "display: none; margin: 0.5em auto;" });
	
				revstab.appendChild(el('tr', [
					el('th', ['#']),
					el('th', ['←']),
					el('th', ['→']),
					el('th', ['Timestamp']),
					el('th', ['Author']),
					el('th', ['Summary'])
				]));
				linkwrap.parentNode.appendChild(revsform);

				var k = 0;
				function grabHistory(cont) {
					var query = {
						action: 'query',
						pageids: mw.config.get('wgArticleId'),
						prop: 'revisions',
						rvprop: 'ids|parsedcomment|user|timestamp|tags|size|flags',
						rvstartid: hiher,
						rvendid: lower,
						rvdir: 'older',
						rvlimit: 'max',
						'continue': ""
					};
					if (cont) {
						for (var key in cont) {
							query[key] = cont[key];
						}
					}
					api.get(query, {
						success: function (result) {
							var wgPageName = mw.config.get('wgPageName');
							if (result.error) {
								mw.util.jsMessage('<b>API error</b>: ' + result.error.info + ' [code: <code>' + result.error.code + '</code>]');
								return;
							}
							var revs = result.query.pages[mw.config.get('wgArticleId')].revisions;
							for (var j = 0; j < revs.length; ++j) {
								var rev = revs[j];
								var sumcell;
								var row = el('tr', [
									el('td', [k++]),
									el('td', [el('input', null, { type: "radio", value: rev.revid, name: "oldid" })]),
									el('td', [el('input', null, { type: "radio", value: rev.revid, name: "diff"  })]),
									el('td', [
										haveRevdel && el('input', null, { type: "checkbox", name: "ids[" + rev.revid + "]", value: "1" }), ' ',
										el('a', [fmtDate(rev.timestamp)], {
											href: mw.util.getUrl(wgPageName, { "oldid": rev.revid })
										}), el('br'), el('small', [
											el('a', ['diff to prev'], { href: mw.util.getUrl(wgPageName, { "oldid": rev.revid, "diff": "prev" }) }),
											' \u2022 ',
											el('a', ['next'], { href: mw.util.getUrl(wgPageName, { "oldid": rev.revid, "diff": "next" }) }),
											' \u2022 ',
											el('a', ['edit'], { href: mw.util.getUrl(wgPageName, { "oldid": rev.revid, "action": "edit" }) }),
										])
									]), 
									el('td', [
										el('a', [rev.user], { href: mw.util.getUrl('User:' + rev.user) }), el('br'),
										el('small', [
											el('a', ['talk'], { href: mw.util.getUrl('User talk:' + rev.user) }),
											' \u2022 ',
											el('a', ['contribs'], { href: mw.util.getUrl('Special:Contributions/' + rev.user) }),
											haveBlock && ' \u2022 ',
											haveBlock && el('a', ['block'], { href: mw.util.getUrl('Special:Block/' + rev.user) }),
										])
									]),
									sumcell = el('td')
								]);
								sumcell.innerHTML = rev.parsedcomment;
								revstab.appendChild(row);
							}
							if (result.continue)
								grabHistory(result['continue']);
						},
						error: function () {
							mw.util.jsMessage('<b>Request error</b>. Details sent to console.');
							console.error(arguments);
						}
					});
				}
				
				grabHistory();
				
				if (revsform.style.display === 'none') {
					linklabel.data = 'hide list';
					revsform.style.display = '';
				} else {
					linklabel.data = 'show list';
					revsform.style.display = 'none';
				}
			});

			
		}
	}), ']'
]));

});

})();