Gebruiker:Xxmarijnw/RTRC.js: verschil tussen versies

Uit Wikikids
Naar navigatie springen Naar zoeken springen
(Kopie van https://meta.wikimedia.org/w/index.php?title=User:Krinkle/RTRC.js&action=raw&ctype=text/javascript)
 
(Kopie van https://github.com/Krinkle/mw-gadget-rtrc/blob/master/src/rtrc.js onder de MIT licentie (https://krinkle.mit-license.org/@2016))
Regel 1: Regel 1:
mw.
+
/**
 +
* Real-Time Recent Changes
 +
* https://github.com/Krinkle/mw-gadget-rtrc
 +
*
 +
* @author Timo Tijhof
 +
* @license https://krinkle.mit-license.org/@2016
 +
*/
 +
/*global alert */
 +
(function ($, mw) {
 +
'use strict';
 +
 
 +
/**
 +
* Configuration
 +
* -------------------------------------------------
 +
*/
 +
var
 +
appVersion = 'v1.2.0',
 +
conf = mw.config.get([
 +
'skin',
 +
'wgAction',
 +
'wgCanonicalSpecialPageName',
 +
'wgPageName',
 +
'wgServer',
 +
'wgTitle',
 +
'wgUserLanguage',
 +
'wgDBname',
 +
'wgScriptPath'
 +
]),
 +
// Can't use mw.util.wikiScript until after #init
 +
apiUrl = conf.wgScriptPath + '/api.php',
 +
cvnApiUrl = '//cvn.wmflabs.org/api.php',
 +
oresApiUrl = '//ores.wikimedia.org/scores/' + conf.wgDBname + '/',
 +
oresModel = false,
 +
intuitionLoadUrl = '//tools.wmflabs.org/intuition/load.php?env=mw',
 +
docUrl = '//meta.wikimedia.org/wiki/User:Krinkle/Tools/Real-Time_Recent_Changes?uselang=' + conf.wgUserLanguage,
 +
// 32x32px
 +
ajaxLoaderUrl = '//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif',
 +
annotationsCache = {
 +
patrolled: {},
 +
cvn: {},
 +
ores: {}
 +
},
 +
// See annotationsCacheUp()
 +
annotationsCacheSize = 0,
 +
 
 +
/**
 +
* Info from the wiki
 +
* -------------------------------------------------
 +
*/
 +
userHasPatrolRight = false,
 +
rcTags = [],
 +
wikiTimeOffset,
 +
 
 +
/**
 +
* State
 +
* -------------------------------------------------
 +
*/
 +
updateFeedTimeout,
 +
 
 +
rcDayHeadPrev,
 +
skippedRCIDs = [],
 +
monthNames,
 +
 
 +
prevFeedHtml,
 +
updateReq,
 +
 
 +
/**
 +
* Feed options
 +
* -------------------------------------------------
 +
*/
 +
defOpt = {
 +
rc: {
 +
// Timestamp
 +
start: undefined,
 +
// Timestamp
 +
end: undefined,
 +
// Direction "older" (descending) or "newer" (ascending)
 +
dir: 'older',
 +
// Array of namespace ids
 +
namespace: undefined,
 +
// User name
 +
user: undefined,
 +
// Tag ID
 +
tag: undefined,
 +
// Filters
 +
hideliu: false,
 +
hidebots: true,
 +
unpatrolled: false,
 +
limit: 25,
 +
// Type filters are "show matches only"
 +
typeEdit: true,
 +
typeNew: true
 +
},
 +
 
 +
app: {
 +
refresh: 5,
 +
cvnDB: false,
 +
ores: false,
 +
massPatrol: false,
 +
autoDiff: false
 +
}
 +
},
 +
aliasOpt = {
 +
// Back-compat for v1.0.4 and earlier
 +
showAnonOnly: 'hideliu',
 +
showUnpatrolledOnly: 'unpatrolled'
 +
},
 +
opt = $(true, {}, defOpt),
 +
 
 +
timeUtil,
 +
message,
 +
msg,
 +
navSupported = conf.skin === 'vector',
 +
rAF = window.requestAnimationFrame || setTimeout,
 +
 
 +
currentDiff,
 +
currentDiffRcid,
 +
$wrapper, $body, $feed,
 +
$RCOptionsSubmit;
 +
 
 +
/**
 +
* Utility functions
 +
* -------------------------------------------------
 +
*/
 +
 
 +
/**
 +
* Prepend a leading zero if value is under 10
 +
*
 +
* @param {number} num Value between 0 and 99.
 +
* @return {string}
 +
*/
 +
function pad(num) {
 +
return (num < 10 ? '0' : '') + num;
 +
}
 +
 
 +
timeUtil = {
 +
// Create new Date object from an ISO-8601 formatted timestamp, as
 +
// returned by the MediaWiki API (e.g. "2010-04-25T23:24:02Z")
 +
newDateFromISO: function (s) {
 +
return new Date(Date.parse(s));
 +
},
 +
 
 +
/**
 +
* Apply user offset
 +
*
 +
* Only use this if you're extracting individual values from the object (e.g. getUTCDay or
 +
* getUTCMinutes). The internal timestamp will be wrong.
 +
*
 +
* @param {Date} d
 +
* @return {Date}
 +
*/
 +
applyUserOffset: function (d) {
 +
var parts,
 +
offset = mw.user.options.get('timecorrection');
 +
 
 +
// This preference has no default value, it is null for users that don't
 +
// override the site's default timeoffset.
 +
if (offset) {
 +
parts = offset.split('|');
 +
if (parts[0] === 'System') {
 +
// Ignore offset value, as system may have started or stopped
 +
// DST since the preferences were saved.
 +
offset = wikiTimeOffset;
 +
} else {
 +
offset = Number(parts[1]);
 +
}
 +
} else {
 +
offset = wikiTimeOffset;
 +
}
 +
// There is no way to set a timezone in javascript, so instead we pretend the
 +
// UTC timestamp is different and use getUTC* methods everywhere.
 +
d.setTime(d.getTime() + (offset * 60 * 1000));
 +
return d;
 +
},
 +
 
 +
// Get clocktime string adjusted to timezone of wiki
 +
// from MediaWiki timestamp string
 +
getClocktimeFromApi: function (s) {
 +
var d = timeUtil.applyUserOffset(timeUtil.newDateFromISO(s));
 +
// Return clocktime with leading zeros
 +
return pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes());
 +
}
 +
};
 +
 
 +
/**
 +
* Main functions
 +
* -----------------------

Versie van 21 jul 2016 22:37

/**
 * Real-Time Recent Changes
 * https://github.com/Krinkle/mw-gadget-rtrc
 *
 * @author Timo Tijhof
 * @license https://krinkle.mit-license.org/@2016
 */
/*global alert */
(function ($, mw) {
	'use strict';

	/**
	 * Configuration
	 * -------------------------------------------------
	 */
	var
	appVersion = 'v1.2.0',
	conf = mw.config.get([
		'skin',
		'wgAction',
		'wgCanonicalSpecialPageName',
		'wgPageName',
		'wgServer',
		'wgTitle',
		'wgUserLanguage',
		'wgDBname',
		'wgScriptPath'
	]),
	// Can't use mw.util.wikiScript until after #init
	apiUrl = conf.wgScriptPath + '/api.php',
	cvnApiUrl = '//cvn.wmflabs.org/api.php',
	oresApiUrl = '//ores.wikimedia.org/scores/' + conf.wgDBname + '/',
	oresModel = false,
	intuitionLoadUrl = '//tools.wmflabs.org/intuition/load.php?env=mw',
	docUrl = '//meta.wikimedia.org/wiki/User:Krinkle/Tools/Real-Time_Recent_Changes?uselang=' + conf.wgUserLanguage,
	// 32x32px
	ajaxLoaderUrl = '//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif',
	annotationsCache = {
		patrolled: {},
		cvn: {},
		ores: {}
	},
	// See annotationsCacheUp()
	annotationsCacheSize = 0,

	/**
	 * Info from the wiki
	 * -------------------------------------------------
	 */
	userHasPatrolRight = false,
	rcTags = [],
	wikiTimeOffset,

	/**
	 * State
	 * -------------------------------------------------
	 */
	updateFeedTimeout,

	rcDayHeadPrev,
	skippedRCIDs = [],
	monthNames,

	prevFeedHtml,
	updateReq,

	/**
	 * Feed options
	 * -------------------------------------------------
	 */
	defOpt = {
		rc: {
			// Timestamp
			start: undefined,
			// Timestamp
			end: undefined,
			// Direction "older" (descending) or "newer" (ascending)
			dir: 'older',
			// Array of namespace ids
			namespace: undefined,
			// User name
			user: undefined,
			// Tag ID
			tag: undefined,
			// Filters
			hideliu: false,
			hidebots: true,
			unpatrolled: false,
			limit: 25,
			// Type filters are "show matches only"
			typeEdit: true,
			typeNew: true
		},

		app: {
			refresh: 5,
			cvnDB: false,
			ores: false,
			massPatrol: false,
			autoDiff: false
		}
	},
	aliasOpt = {
		// Back-compat for v1.0.4 and earlier
		showAnonOnly: 'hideliu',
		showUnpatrolledOnly: 'unpatrolled'
	},
	opt = $(true, {}, defOpt),

	timeUtil,
	message,
	msg,
	navSupported = conf.skin === 'vector',
	rAF = window.requestAnimationFrame || setTimeout,

	currentDiff,
	currentDiffRcid,
	$wrapper, $body, $feed,
	$RCOptionsSubmit;

	/**
	 * Utility functions
	 * -------------------------------------------------
	 */

	/**
	 * Prepend a leading zero if value is under 10
	 *
	 * @param {number} num Value between 0 and 99.
	 * @return {string}
	 */
	function pad(num) {
		return (num < 10 ? '0' : '') + num;
	}

	timeUtil = {
		// Create new Date object from an ISO-8601 formatted timestamp, as
		// returned by the MediaWiki API (e.g. "2010-04-25T23:24:02Z")
		newDateFromISO: function (s) {
			return new Date(Date.parse(s));
		},

		/**
		 * Apply user offset
		 *
		 * Only use this if you're extracting individual values from the object (e.g. getUTCDay or
		 * getUTCMinutes). The internal timestamp will be wrong.
		 *
		 * @param {Date} d
		 * @return {Date}
		 */
		applyUserOffset: function (d) {
			var parts,
				offset = mw.user.options.get('timecorrection');

			// This preference has no default value, it is null for users that don't
			// override the site's default timeoffset.
			if (offset) {
				parts = offset.split('|');
				if (parts[0] === 'System') {
					// Ignore offset value, as system may have started or stopped
					// DST since the preferences were saved.
					offset = wikiTimeOffset;
				} else {
					offset = Number(parts[1]);
				}
			} else {
				offset = wikiTimeOffset;
			}
			// There is no way to set a timezone in javascript, so instead we pretend the
			// UTC timestamp is different and use getUTC* methods everywhere.
			d.setTime(d.getTime() + (offset * 60 * 1000));
			return d;
		},

		// Get clocktime string adjusted to timezone of wiki
		// from MediaWiki timestamp string
		getClocktimeFromApi: function (s) {
			var d = timeUtil.applyUserOffset(timeUtil.newDateFromISO(s));
			// Return clocktime with leading zeros
			return pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes());
		}
	};

	/**
	 * Main functions
	 * -------------------------------------------------
	 */

	/**
	 * @param {Date} date
	 * @return {string} HTML
	 */
	function buildRcDayHead(date) {
		var current = date.getDate();
		if (current === rcDayHeadPrev) {
			return '';
		}
		rcDayHeadPrev = current;
		return '<div class="mw-rtrc-heading"><div><strong>' + date.getDate() + ' ' + monthNames[date.getMonth()] + '</strong></div></div>';
	}

	/**
	 * @param {Object} rc Recent change object from API
	 * @return {string} HTML
	 */
	function buildRcItem(rc) {
		var diffsize, isUnpatrolled, isAnon, typeSymbol, itemClass, diffLink, el, item;

		// Get size difference (can be negative, zero or positive)
		diffsize = rc.newlen - rc.oldlen;

		// Convert undefined/empty-string values from API into booleans
		isUnpatrolled = rc.unpatrolled !== undefined;
		isAnon = rc.anon !== undefined;

		// typeSymbol, diffLink & itemClass
		typeSymbol = '&nbsp;';
		itemClass = [];

		if (rc.type === 'new') {
			typeSymbol += '<span class="newpage">N</span>';
		}

		if ((rc.type === 'edit' || rc.type === 'new') && userHasPatrolRight && isUnpatrolled) {
			typeSymbol += '<span class="unpatrolled">!</span>';
		}

		if (rc.oldlen > 0 && rc.newlen === 0) {
			itemClass.push('mw-rtrc-item-alert');
		}

		/*
Example:

<div class="mw-rtrc-item mw-rtrc-item-patrolled" data-diff="0" data-rcid="0" user="Abc">
	<div first>(<a>diff</a>) <span class="unpatrolled">!</span> 00:00 <a>Page</a></div>
	<div user><a class="user" href="//User:Abc">Abc</a></div>
	<div comment><a href="//User talk:Abc">talk</a> / <a href="//Special:Contributions/Abc">contribs</a>&nbsp;<span class="comment">Abc</span></div>
	<div class="mw-rtrc-meta"><span class="mw-plusminus mw-plusminus-null">(0)</span></div>
</div>
		*/

		// build & return item
		item = buildRcDayHead(timeUtil.newDateFromISO(rc.timestamp));
		item += '<div class="mw-rtrc-item ' + itemClass.join(' ') + '" data-diff="' + rc.revid + '" data-rcid="' + rc.rcid + '" user="' + rc.user + '">';

		if (rc.type === 'edit') {
			diffLink = '<a class="rcitemlink diff" href="' +
				mw.util.wikiScript() + '?diff=' + rc.revid + '&oldid=' + rc.old_revid + '&rcid=' + rc.rcid +
				'">' + mw.message('diff').escaped() + '</a>';
		} else if (rc.type === 'new') {
			diffLink = '<a class="rcitemlink newPage">new</a>';
		} else {
			diffLink = mw.message('diff').escaped();
		}

		item += '<div first>' +
			'(' + diffLink + ') ' + typeSymbol + ' ' +
			timeUtil.getClocktimeFromApi(rc.timestamp) +
			' <a class="page" href="' + mw.util.getUrl(rc.title) + '?rcid=' + rc.rcid + '" target="_blank">' + rc.title + '</a>' +
			'</div>' +
			'<div user>&nbsp;<small>&middot;&nbsp;' +
			'<a href="' + mw.util.getUrl('User talk:' + rc.user) + '" target="_blank">' + mw.message('talkpagelinktext').escaped() + '</a>' +
			' &middot; ' +
			'<a href="' + mw.util.getUrl('Special:Contributions/' + rc.user) + '" target="_blank">' + mw.message('contribslink').escaped() + '</a>' +
			'&nbsp;</small>&middot;&nbsp;' +
			'<a class="mw-userlink" href="' + mw.util.getUrl((mw.util.isIPv4Address(rc.user) || mw.util.isIPv6Address(rc.user) ? 'Special:Contributions/' : 'User:') + rc.user) + '" target="_blank">' + rc.user + '</a>' +
			'</div>' +
			'<div comment>&nbsp;<span class="comment">' + rc.parsedcomment + '</span></div>';

		if (diffsize > 0) {
			el = diffsize > 399 ? 'strong' : 'span';
			item += '<div class="mw-rtrc-meta"><' + el + ' class="mw-plusminus mw-plusminus-pos">(+' + diffsize.toLocaleString() + ')</' + el + '></div>';
		} else if (diffsize === 0) {
			item += '<div class="mw-rtrc-meta"><span class="mw-plusminus mw-plusminus-null">(0)</span></div>';
		} else {
			el = diffsize < -399 ? 'strong' : 'span';
			item += '<div class="mw-rtrc-meta"><' + el + ' class="mw-plusminus mw-plusminus-neg">(' + diffsize.toLocaleString() + ')</' + el + '></div>';
		}

		item += '</div>';
		return item;
	}

	/**
	 * @param {Object} newOpt
	 * @param {string} [mode=normal] One of 'quiet' or 'normal'
	 * @return {boolean} True if no changes were made, false otherwise
	 */
	function normaliseSettings(newOpt, mode) {
		var mod = false;

		// MassPatrol requires a filter to be active
		if (newOpt.app.massPatrol && !newOpt.rc.user) {
			newOpt.app.massPatrol = false;
			mod = true;
			if (mode !== 'quiet') {
				alert(msg('masspatrol-requires-userfilter'));
			}
		}

		// MassPatrol implies AutoDiff
		if (newOpt.app.massPatrol && !newOpt.app.autoDiff) {
			newOpt.app.autoDiff = true;
			mod = true;
		}
		// MassPatrol implies fetching only unpatrolled changes
		if (newOpt.app.massPatrol && !newOpt.rc.unpatrolled) {
			newOpt.rc.unpatrolled = true;
			mod = true;
		}

		return !mod;
	}

	function fillSettingsForm(newOpt) {
		var $settings = $($wrapper.find('.mw-rtrc-settings')[0].elements).filter(':input');

		if (newOpt.rc) {
			$.each(newOpt.rc, function (key, value) {
				var $setting = $settings.filter(function () {
						return this.name === key;
					}),
					setting = $setting[0];

				if (!setting) {
					return;
				}

				switch (key) {
				case 'limit':
					setting.value = value;
					break;
				case 'namespace':
					if (value === undefined) {
						// Value "" (all) is represented by undefined.
						$setting.find('option').eq(0).prop('selected', true);
					} else {
						$setting.val(value);
					}
					break;
				case 'user':
				case 'start':
				case 'end':
				case 'tag':
					setting.value = value || '';
					break;
				case 'hideliu':
				case 'hidebots':
				case 'unpatrolled':
				case 'typeEdit':
				case 'typeNew':
					setting.checked = value;
					break;
				case 'dir':
					if (setting.value === value) {
						setting.checked = true;
					}
					break;
				}
			});
		}

		if (newOpt.app) {
			$.each(newOpt.app, function (key, value) {
				var $setting = $settings.filter(function () {
						return this.name === key;
					}),
					setting = $setting[0];

				if (!setting) {
					setting = document.getElementById('rc-options-' + key);
					$setting = $(setting);
				}

				if (!setting) {
					return;
				}

				switch (key) {
				case 'cvnDB':
				case 'ores':
				case 'massPatrol':
				case 'autoDiff':
					setting.checked = value;
					break;
				case 'refresh':
					setting.value = value;
					break;
				}
			});
		}

	}

	function readSettingsForm() {
		// jQuery#serializeArray is nice, but doesn't include "value: false" for unchecked
		// checkboxes that are not disabled. Using raw .elements instead and filtering
		// out <fieldset>.
		var $settings = $($wrapper.find('.mw-rtrc-settings')[0].elements).filter(':input');

		opt = $.extend(true, {}, defOpt);

		$settings.each(function (i, el) {
			var name = el.name;

			switch (name) {
			// RC
			case 'limit':
				opt.rc[name] = Number(el.value);
				break;
			case 'namespace':
				// Can be "0".
				// Value "" (all) is represented by undefined.
				// TODO: Turn this into a multi-select, the API supports it.
				opt.rc[name] = el.value.length ? Number(el.value) : undefined;
				break;
			case 'user':
			case 'start':
			case 'end':
			case 'tag':
				opt.rc[name] = el.value || undefined;
				break;
			case 'hideliu':
			case 'hidebots':
			case 'unpatrolled':
			case 'typeEdit':
			case 'typeNew':
				opt.rc[name] = el.checked;
				break;
			case 'dir':
				// There's more than 1 radio button with this name in this loop,
				// use the value of the first (and only) checked one.
				if (el.checked) {
					opt.rc[name] = el.value;
				}
				break;
			// APP
			case 'cvnDB':
			case 'ores':
			case 'massPatrol':
			case 'autoDiff':
				opt.app[name] = el.checked;
				break;
			case 'refresh':
				opt.app[name] = Number(el.value);
				break;
			}
		});

		if (!normaliseSettings(opt)) {
			// TODO: Optimise this, no need to repopulate the entire settings form
			// if only 1 thing changed.
			fillSettingsForm(opt);
		}
	}

	function getPermalink() {
		var uri = new mw.Uri(mw.util.getUrl(conf.wgPageName)),
			reducedOpt = {};

		$.each(opt.rc, function (key, value) {
			if (defOpt.rc[key] !== value) {
				if (!reducedOpt.rc) {
					reducedOpt.rc = {};
				}
				reducedOpt.rc[key] = value;
			}
		});

		$.each(opt.app, function (key, value) {
			// Don't permalink MassPatrol (issue Krinkle/mw-rtrc-gadget#59)
			if (key !== 'massPatrol' && defOpt.app[key] !== value) {
				if (!reducedOpt.app) {
					reducedOpt.app = {};
				}
				reducedOpt.app[key] = value;
			}
		});

		reducedOpt = JSON.stringify(reducedOpt);

		uri.extend({
			opt: reducedOpt === '{}' ? '' : reducedOpt
		});

		return uri.toString();
	}

	function updateFeedNow() {
		$('#rc-options-pause').prop('checked', false);
		if (updateReq) {
			// Try to abort the current request
			updateReq.abort();
		}
		clearTimeout(updateFeedTimeout);
		return updateFeed();
	}

	/**
	 * @param {jQuery} $element
	 */
	function scrollIntoView($element) {
		$element[0].scrollIntoView({ block: 'start', behavior: 'smooth' });
	}

	/**
	 * @param {jQuery} $element
	 */
	function scrollIntoViewIfNeeded($element) {
		if ($element[0].scrollIntoViewIfNeeded) {
			$element[0].scrollIntoViewIfNeeded({ block: 'start', behavior: 'smooth' });
		} else {
			$element[0].scrollIntoView({ block: 'start', behavior: 'smooth' });
		}
	}

	// Read permalink into the program and reflect into settings form.
	function readPermalink() {
		var group, oldKey, newKey, newOpt,
			url = new mw.Uri();

		if (url.query.opt) {
			try {
				newOpt = JSON.parse(url.query.opt);
			} catch (e) {
				// TODO: Report error to user
			}
		}
		if (newOpt) {
			// Rename values for old aliases
			for (group in newOpt) {
				for (oldKey in newOpt[group]) {
					newKey = aliasOpt[oldKey];
					if (newKey && !newOpt[group].hasOwnProperty(newKey)) {
						newOpt[group][newKey] = newOpt[group][oldKey];
						delete newOpt[group][oldKey];
					}
				}
			}

			if (newOpt.app) {
				// Don't permalink MassPatrol (issue Krinkle/mw-rtrc-gadget#59)
				delete newOpt.app.massPatrol;
			}
		}

		newOpt = $.extend(true, {}, defOpt, newOpt);

		normaliseSettings(newOpt, 'quiet');
		fillSettingsForm(newOpt);

		opt = newOpt;
	}

	function getApiRcParams(rc) {
		var params,
			rcprop = [
				'flags',
				'timestamp',
				'user',
				'title',
				'parsedcomment',
				'sizes',
				'ids'
			],
			rcshow = [],
			rctype = [];

		if (userHasPatrolRight) {
			rcprop.push('patrolled');
		}

		if (rc.hideliu) {
			rcshow.push('anon');
		}
		if (rc.hidebots) {
			rcshow.push('!bot');
		}
		if (rc.unpatrolled) {
			rcshow.push('!patrolled');
		}

		if (rc.typeEdit) {
			rctype.push('edit');
		}
		if (rc.typeNew) {
			rctype.push('new');
		}
		if (!rctype.length) {
			// Custom default instead of MediaWiki's default (in case both checkboxes were unchecked)
			rctype = ['edit', 'new'];
		}

		params = {
			rcdir: rc.dir,
			rclimit: rc.limit,
			rcshow: rcshow.join('|'),
			rcprop: rcprop.join('|'),
			rctype: rctype.join('|')
		};

		if (rc.dir === 'older') {
			if (rc.end !== undefined) {
				params.rcstart = rc.end;
			}
			if (rc.start !== undefined) {
				params.rcend = rc.start;
			}
		} else if (rc.dir === 'newer') {
			if (rc.start !== undefined) {
				params.rcstart = rc.start;
			}
			if (rc.end !== undefined) {
				params.rcend = rc.end;
			}
		}

		if (rc.namespace !== undefined) {
			params.rcnamespace = rc.namespace;
		}

		if (rc.user !== undefined) {
			params.rcuser = rc.user;
		}

		if (rc.tag !== undefined) {
			params.rctag = rc.tag;
		}

		// params.titles: Title filter (rctitles) is no longer supported by MediaWiki,
		// see https://bugzilla.wikimedia.org/show_bug.cgi?id=12394#c5.

		return params;
	}

	// Called when the feed is regenerated before being inserted in the document
	function applyRtrcAnnotations($feedContent) {
		// Re-apply item classes
		$feedContent.filter('.mw-rtrc-item').each(function () {
			var $el = $(this),
				rcid = Number($el.data('rcid'));

			// Mark skipped and patrolled items as such
			if ($.inArray(rcid, skippedRCIDs) !== -1) {
				$el.addClass('mw-rtrc-item-skipped');
			} else if (annotationsCache.patrolled.hasOwnProperty(rcid)) {
				$el.addClass('mw-rtrc-item-patrolled');
			} else if (rcid === currentDiffRcid) {
				$el.addClass('mw-rtrc-item-current');
			}
		});
	}

	function applyOresAnnotations($feedContent) {
		var dAnnotations, revids, fetchRevids;

		if (!oresModel) {
			return $.Deferred().resolve();
		}

		// Find all revids names inside the feed
		revids = $.map($feedContent.filter('.mw-rtrc-item'), function (node) {
			return $(node).attr('data-diff');
		});

		if (!revids.length) {
			return $.Deferred().resolve();
		}

		fetchRevids = $.grep(revids, function (revid) {
			return !annotationsCache.ores.hasOwnProperty(revid);
		});

		if (!fetchRevids.length) {
			// No (new) revisions
			dAnnotations = $.Deferred().resolve(annotationsCache.ores);
		} else {
			dAnnotations = $.ajax({
				url: oresApiUrl,
				data: {
					models: oresModel,
					revids: fetchRevids.join('|')
				},
				timeout: 10000,
				dataType: $.support.cors ? 'json' : 'jsonp',
				cache: true
			}).then(function (resp) {
				var len;
				if (resp) {
					len = Object.keys ? Object.keys(resp).length : fetchRevids.length;
					annotationsCacheUp(len);
					$.each(resp, function (revid, item) {
						if (!item || item.error || !item[oresModel] || item[oresModel].error) {
							return;
						}
						annotationsCache.ores[revid] = item[oresModel].probability['true'];
					});
				}
				return annotationsCache.ores;
			});
		}

		return dAnnotations.then(function (annotations) {
			// Loop through all revision ids
			$.each(revids, function (i, revid) {
				var tooltip,
					score = annotations[revid];
				// Only highlight high probability scores
				if (!score || score <= 0.45) {
					return;
				}
				tooltip = msg('ores-damaging-probability', (100 * score).toFixed(0) + '%');

				// Add alert
				$feedContent
					.filter('.mw-rtrc-item[data-diff="' + Number(revid) + '"]')
					.addClass('mw-rtrc-item-alert mw-rtrc-item-alert-rev')
					.find('.mw-rtrc-meta')
					.prepend(
						$('<span>')
							.addClass('mw-rtrc-revscore')
							.attr('title', tooltip)
					);
			});
		});
	}

	function applyCvnAnnotations($feedContent) {
		var dAnnotations,
			users = [];

		// Collect user names
		$feedContent.filter('.mw-rtrc-item').each(function () {
			var user = $(this).attr('user');
			// Don't query the same user multiple times
			if (user && $.inArray(user, users) === -1 && !annotationsCache.cvn.hasOwnProperty(user)) {
				users.push(user);
			}
		});

		if (!users.length) {
			// No (new) users
			dAnnotations = $.Deferred().resolve(annotationsCache.cvn);
		} else {
			dAnnotations = $.ajax({
				url: cvnApiUrl,
				data: { users: users.join('|') },
				timeout: 2000,
				dataType: $.support.cors ? 'json' : 'jsonp',
				cache: true
			})
			.then(function (resp) {
				if (resp.users) {
					annotationsCacheUp(resp.users.length);
					$.each(resp.users, function (name, user) {
						annotationsCache.cvn[name] = user;
					});
				}
				return annotationsCache.cvn;
			});
		}

		return dAnnotations.then(function (annotations) {
			// Loop through all cvn user annotations
			$.each(annotations, function (name, user) {
				var tooltip;

				// Only if blacklisted, otherwise don't highlight
				if (user.type === 'blacklist') {
					tooltip = '';

					if (user.comment) {
						tooltip += msg('cvn-reason') + ': ' + user.comment + '. ';
					} else {
						tooltip += msg('cvn-reason') + ': ' + msg('cvn-reason-empty');
					}

					if (user.adder) {
						tooltip += msg('cvn-adder') + ': ' + user.adder;
					} else {
						tooltip += msg('cvn-adder') + ': ' + msg('cvn-adder-empty');
					}

					// Add alert
					$feedContent
						.filter('.mw-rtrc-item')
						.filter(function () {
							return $(this).attr('user') === name;
						})
						.addClass('mw-rtrc-item-alert mw-rtrc-item-alert-user')
						.find('.mw-userlink')
						.attr('title', tooltip);
				}

			});
		});
	}

	/**
	 * @param {Object} update
	 * @param {jQuery} update.$feedContent
	 * @param {string} update.rawHtml
	 */
	function pushFeedContent(update) {
		$body.removeClass('placeholder');

		$feed.find('.mw-rtrc-feed-update').html(
			message('lastupdate-rc', new Date().toLocaleString()).escaped() +
			' | <a href="' + mw.html.escape(getPermalink()) + '">' +
			message('permalink').escaped() +
			'</a>'
		);

		if (update.rawHtml !== prevFeedHtml) {
			prevFeedHtml = update.rawHtml;
			applyRtrcAnnotations(update.$feedContent);
			$feed.find('.mw-rtrc-feed-content').empty().append(update.$feedContent);
		}
	}

	function updateFeed() {
		if (updateReq) {
			updateReq.abort();
		}

		// Indicate updating
		$('#krRTRC_loader').show();

		// Download recent changes
		updateReq = $.ajax({
			url: apiUrl,
			dataType: 'json',
			data: $.extend(getApiRcParams(opt.rc), {
				format: 'json',
				action: 'query',
				list: 'recentchanges'
			})
		});
		// This waterfall flows in one of two ways:
		// - Everything casts to success and results in a UI update (maybe an error message),
		//   loading indicator hidden, and the next update scheduled.
		// - Request is aborted and nothing happens (instead, the final handling will
		//   be done by the new request).
		return updateReq.always(function () {
			updateReq = null;
		})
		.then(null, function (jqXhr, textStatus) {
			var feedContentHTML = '<h3>Downloading recent changes failed</h3>';
			if (textStatus === 'abort') {
				return $.Deferred().reject();
			}
			pushFeedContent({
				$feedContent: $(feedContentHTML),
				rawHtml: feedContentHTML
			});
			// Error is handled. Move on normally.
			return $.Deferred().resolve();
		}).then(function (data) {
			var recentchanges, $feedContent, client,
				feedContentHTML = '';

			if (data.error) {
				// Account doesn't have patrol flag
				if (data.error.code === 'rcpermissiondenied') {
					feedContentHTML += '<h3>Downloading recent changes failed</h3><p>Please untick the "Unpatrolled only"-checkbox or request the Patroller-right.</a>';

				// Other error
				} else {
					client = $.client.profile();
					feedContentHTML += '<h3>Downloading recent changes failed</h3>' +
						'<p>Please check the settings above and try again. If you believe this is a bug, please <strong>' +
						'<a href="https://github.com/Krinkle/mw-gadget-rtrc/issues/new?body=' + encodeURIComponent('\n\n\n----' +
						'\npackage: mw-gadget-rtrc ' + appVersion +
						mw.format('\nbrowser: $1 $2 ($3)', client.name, client.version, client.platform)
						) + '" target="_blank">let me know</a></strong>.';
				}
			} else {
				recentchanges = data.query.recentchanges;

				if (recentchanges.length) {
					$.each(recentchanges, function (i, rc) {
						feedContentHTML += buildRcItem(rc);
					});
				} else {
					// Everything is OK - no results
					feedContentHTML += '<strong><em>' + message('nomatches').escaped() + '</em></strong>';
				}

				// Reset day
				rcDayHeadPrev = undefined;
			}

			$feedContent = $($.parseHTML(feedContentHTML));
			return $.when(
				opt.app.cvnDB && applyCvnAnnotations($feedContent),
				oresModel && opt.app.ores && applyOresAnnotations($feedContent)
			).then(null, function () {
				// Ignore errors from annotation handlers
				return $.Deferred().resolve();
			}).then(function () {
				pushFeedContent({
					$feedContent: $feedContent,
					rawHtml: feedContentHTML
				});
			});
		}).then(function () {
			$RCOptionsSubmit.prop('disabled', false).css('opacity', '1.0');

			// Schedule next update
			updateFeedTimeout = setTimeout(updateFeed, opt.app.refresh * 1000);
			$('#krRTRC_loader').hide();
		});
	}

	function nextDiff() {
		var $lis = $feed.find('.mw-rtrc-item:not(.mw-rtrc-item-current, .mw-rtrc-item-patrolled, .mw-rtrc-item-skipped)');
		$lis.eq(0).find('a.rcitemlink').click();
	}

	function wakeupMassPatrol(settingVal) {
		if (settingVal === true) {
			if (!currentDiff) {
				nextDiff();
			} else {
				$('.patrollink a').click();
			}
		}
	}

	// Build the main interface
	function buildInterface() {
		var namespaceOptionsHtml, tagOptionsHtml, key,
			fmNs = mw.config.get('wgFormattedNamespaces');

		namespaceOptionsHtml = '<option value>' + mw.message('namespacesall').escaped() + '</option>';
		namespaceOptionsHtml += '<option value="0">' + mw.message('blanknamespace').escaped() + '</option>';

		for (key in fmNs) {
			if (key > 0) {
				namespaceOptionsHtml += '<option value="' + key + '">' + fmNs[key] + '</option>';
			}
		}

		tagOptionsHtml = '<option value selected>' + message('select-placeholder-none').escaped() + '</option>';
		for (key = 0; key < rcTags.length; key++) {
			tagOptionsHtml += '<option value="' + mw.html.escape(rcTags[key]) + '">' + mw.html.escape(rcTags[key]) + '</option>';
		}

		$wrapper = $($.parseHTML(
		'<div class="mw-rtrc-wrapper">' +
			'<div class="mw-rtrc-head">' +
				message('title').escaped() + ' <small>(' + appVersion + ')</small>' +
				'<div class="mw-rtrc-head-links">' +
					(!mw.user.isAnon() ? (
						'<a target="_blank" href="' + mw.util.getUrl('Special:Log', { type: 'patrol', user: mw.user.getName(), subtype: 'patrol' }) + '">' +
							message('mypatrollog').escaped() +
						'</a>'
					) : '') +
					'<a id="mw-rtrc-toggleHelp">' + message('help').escaped() + '</a>' +
				'</div>' +
			'</div>' +
			'<form id="krRTRC_RCOptions" class="mw-rtrc-settings mw-rtrc-nohelp make-switch"><fieldset>' +
				'<div class="panel-group">' +
					'<div class="panel">' +
						'<label class="head">' + message('filter').escaped() + '</label>' +
						'<div class="sub-panel">' +
							'<label>' +
								'<input type="checkbox" name="hideliu" />' +
								' ' + message('filter-hideliu').escaped() +
							'</label>' +
							'<br />' +
							'<label>' +
								'<input type="checkbox" name="hidebots" />' +
								' ' + message('filter-hidebots').escaped() +
							'</label>' +
						'</div>' +
						'<div class="sub-panel">' +
							'<label>' +
								'<input type="checkbox" name="unpatrolled" />' +
								' ' + message('filter-unpatrolled').escaped() +
							'</label>' +
							'<br />' +
							'<label>' +
								message('userfilter').escaped() +
								'<span section="Userfilter" class="helpicon"></span>: ' +
								'<input type="search" size="16" name="user" />' +
							'</label>' +
						'</div>' +
					'</div>' +
					'<div class="panel">' +
						'<label class="head">' + message('type').escaped() + '</label>' +
						'<div class="sub-panel">' +
							'<label>' +
								'<input type="checkbox" name="typeEdit" checked />' +
								' ' + message('typeEdit').escaped() +
							'</label>' +
							'<br />' +
							'<label>' +
								'<input type="checkbox" name="typeNew" checked />' +
								' ' + message('typeNew').escaped() +
							'</label>' +
						'</div>' +
					'</div>' +
					'<div class="panel">' +
						'<label  class="head">' +
							mw.message('namespaces').escaped() +
							' <br />' +
							'<select class="mw-rtrc-setting-select" name="namespace">' +
							namespaceOptionsHtml +
							'</select>' +
						'</label>' +
					'</div>' +
					'<div class="panel">' +
						'<label class="head">' +
							message('timeframe').escaped() +
							'<span section="Timeframe" class="helpicon"></span>' +
						'</label>' +
						'<div class="sub-panel" style="text-align: right;">' +
							'<label>' +
								message('time-from').escaped() + ': ' +
								'<input type="text" size="16" placeholder="YYYYMMDDHHIISS" name="start" />' +
							'</label>' +
							'<br />' +
							'<label>' +
								message('time-untill').escaped() + ': ' +
								'<input type="text" size="16" placeholder="YYYYMMDDHHIISS" name="end" />' +
							'</label>' +
						'</div>' +
					'</div>' +
					'<div class="panel">' +
						'<label class="head">' +
							message('order').escaped() +
							' <br />' +
							'<span section="Order" class="helpicon"></span>' +
						'</label>' +
						'<div class="sub-panel">' +
							'<label>' +
								'<input type="radio" name="dir" value="newer" />' +
								' ' + message('asc').escaped() +
							'</label>' +
							'<br />' +
							'<label>' +
								'<input type="radio" name="dir" value="older" checked />' +
								' ' + message('desc').escaped() +
							'</label>' +
						'</div>' +
					'</div>' +
					'<div class="panel">' +
						'<label for="mw-rtrc-settings-refresh" class="head">' +
							message('reload-interval').escaped() + '<br />' +
							'<span section="Reload_Interval" class="helpicon"></span>' +
						'</label>' +
						'<input type="number" value="3" min="0" max="99" size="2" id="mw-rtrc-settings-refresh" name="refresh" />' +
					'</div>' +
					'<div class="panel panel-last">' +
						'<input class="button" type="button" id="RCOptions_submit" value="' + message('apply').escaped() + '" />' +
					'</div>' +
				'</div>' +
				'<div class="panel-group panel-group-mini">' +
					'<div class="panel">' +
						'<label for="mw-rtrc-settings-limit" class="head">' + message('limit').escaped() + '</label>' +
						' <select id="mw-rtrc-settings-limit" name="limit">' +
							'<option value="10">10</option>' +
							'<option value="25" selected>25</option>' +
							'<option value="50">50</option>' +
							'<option value="75">75</option>' +
							'<option value="100">100</option>' +
							'<option value="250">250</option>' +
							'<option value="500">500</option>' +
						'</select>' +
					'</div>' +
					'<div class="panel">' +
						'<label class="head">' +
							message('tag').escaped() +
							' <select class="mw-rtrc-setting-select" name="tag">' +
							tagOptionsHtml +
							'</select>' +
						'</label>' +
					'</div>' +
					'<div class="panel">' +
						'<label class="head">' +
							'CVN Scores' +
							'<span section="CVN_Scores" class="helpicon"></span>' +
							'<input type="checkbox" class="switch" name="cvnDB" />' +
						'</label>' +
					'</div>' +
					(oresModel ? (
						'<div class="panel">' +
							'<label class="head">' +
								'ORES Scores' +
								'<span section="ORES_Scores" class="helpicon"></span>' +
								'<input type="checkbox" class="switch" name="ores" />' +
							'</label>' +
						'</div>'
					) : '') +
					'<div class="panel">' +
						'<label class="head">' +
							message('masspatrol').escaped() +
							'<span section="MassPatrol" class="helpicon"></span>' +
							'<input type="checkbox" class="switch" name="massPatrol" />' +
						'</label>' +
					'</div>' +
					'<div class="panel">' +
						'<label class="head">' +
							message('autodiff').escaped() +
							'<span section="AutoDiff" class="helpicon"></span>' +
							'<input type="checkbox" class="switch" name="autoDiff" />' +
						'</label>' +
					'</div>' +
					'<div class="panel">' +
						'<label class="head">' +
							message('pause').escaped() +
							'<input class="switch" type="checkbox" id="rc-options-pause" />' +
						'</label>' +
					'</div>' +
				'</div>' +
			'</fieldset></form>' +
			'<a name="krRTRC_DiffTop" />' +
			'<div class="mw-rtrc-diff mw-rtrc-diff-closed" id="krRTRC_DiffFrame"></div>' +
			'<div class="mw-rtrc-body placeholder">' +
				'<div class="mw-rtrc-feed">' +
					'<div class="mw-rtrc-feed-update"></div>' +
					'<div class="mw-rtrc-feed-content"></div>' +
				'</div>' +
				'<img src="' + ajaxLoaderUrl + '" id="krRTRC_loader" style="display: none;" />' +
				'<div class="mw-rtrc-legend">' +
					message('legend').escaped() + ': ' +
					'<div class="mw-rtrc-item mw-rtrc-item-patrolled">' + mw.message('markedaspatrolled').escaped() + '</div>, ' +
					'<div class="mw-rtrc-item mw-rtrc-item-current">' + message('currentedit').escaped() + '</div>, ' +
					'<div class="mw-rtrc-item mw-rtrc-item-skipped">' + message('skippededit').escaped() + '</div>' +
				'</div>' +
			'</div>' +
			'<div style="clear: both;"></div>' +
			'<div class="mw-rtrc-foot">' +
				'<div class="plainlinks" style="text-align: right;">' +
					'Real-Time Recent Changes by ' +
					'<a href="//meta.wikimedia.org/wiki/User:Krinkle">Krinkle</a>' +
					' | <a href="' + docUrl + '">' + message('documentation').escaped() + '</a>' +
					' | <a href="https://github.com/Krinkle/mw-gadget-rtrc/releases">' + message('changelog').escaped() + '</a>' +
					' | <a href="https://github.com/Krinkle/mw-gadget-rtrc/issues">Feedback</a>' +
					' | <a href="https://krinkle.mit-license.org/@2016">License</a>' +
				'</div>' +
			'</div>' +
		'</div>'
		));

		// Add helper element for switch checkboxes
		$wrapper.find('input.switch').after('<div class="switched"></div>');

		// All links within the diffframe should open in a new window
		$wrapper.find('#krRTRC_DiffFrame').on('click', 'table.diff a', function () {
			var $el = $(this);
			if ($el.is('[href^="http://"], [href^="https://"], [href^="//"]')) {
				$el.attr('target', '_blank');
			}
		});

		$('#content').empty().append($wrapper);

		$body = $wrapper.find('.mw-rtrc-body');
		$feed = $body.find('.mw-rtrc-feed');
	}

	function annotationsCacheUp(increment) {
		annotationsCacheSize += increment || 1;
		if (annotationsCacheSize > 1000) {
			annotationsCache.patrolled = {};
			annotationsCache.ores = {};
			annotationsCache.cvn = {};
		}
	}

	// Bind event hanlders in the user interface
	function bindInterface() {
		var api = new mw.Api();
		$RCOptionsSubmit = $('#RCOptions_submit');

		// Apply button
		$RCOptionsSubmit.click(function () {
			$RCOptionsSubmit.prop('disabled', true).css('opacity', '0.5');

			readSettingsForm();

			updateFeedNow().then(function () {
				wakeupMassPatrol(opt.app.massPatrol);
			});
			return false;
		});

		// Close Diff
		$wrapper.on('click', '#diffClose', function () {
			$('#krRTRC_DiffFrame').addClass('mw-rtrc-diff-closed');
			currentDiff = currentDiffRcid = false;
		});

		// Load diffview on (diff)-link click
		$feed.on('click', 'a.diff', function (e) {
			var $item = $(this).closest('.mw-rtrc-item').addClass('mw-rtrc-item-current'),
				title = $item.find('.page').text(),
				href = $(this).attr('href'),
				$frame = $('#krRTRC_DiffFrame');

			$feed.find('.mw-rtrc-item-current').not($item).removeClass('mw-rtrc-item-current');

			currentDiff = Number($item.data('diff'));
			currentDiffRcid = Number($item.data('rcid'));

			$frame
				.addClass('mw-rtrc-diff-loading')
				// Reset class potentially added by a.newPage or diffClose
				.removeClass('mw-rtrc-diff-newpage mw-rtrc-diff-closed');

			$.ajax({
				url: mw.util.wikiScript(),
				dataType: 'html',
				data: {
					action: 'render',
					diff: currentDiff,
					diffonly: '1',
					uselang: conf.wgUserLanguage
				}
			}).fail(function (jqXhr) {
				$frame
					.append(jqXhr.responseText || 'Loading diff failed.')
					.removeClass('mw-rtrc-diff-loading');
			}).done(function (data) {
				var skipButtonHtml, $diff;
				if ($.inArray(currentDiffRcid, skippedRCIDs) !== -1) {
					skipButtonHtml = '<span class="tab"><a id="diffUnskip">' + message('unskip').escaped() + '</a></span>';
				} else {
					skipButtonHtml = '<span class="tab"><a id="diffSkip">' + message('skip').escaped() + '</a></span>';
				}

				$frame
					.html(data)
					.prepend(
						'<h3>' + mw.html.escape(title) + '</h3>' +
						'<div class="mw-rtrc-diff-tools">' +
							'<span class="tab"><a id="diffClose">' + message('close').escaped() + '</a></span>' +
							'<span class="tab"><a href="' + href + '" target="_blank" id="diffNewWindow">Open in Wiki</a></span>' +
							(userHasPatrolRight ?
								'<span class="tab"><a onclick="(function(){ if($(\'.patrollink a\').length){ $(\'.patrollink a\').click(); } else { $(\'#diffSkip\').click(); } })();">[mark]</a></span>' :
								''
							) +
							'<span class="tab"><a id="diffNext">' + mw.message('next').escaped() + ' &raquo;</a></span>' +
							skipButtonHtml +
						'</div>'
					)
					.removeClass('mw-rtrc-diff-loading');

				if (opt.app.massPatrol) {
					$frame.find('.patrollink a').click();
				} else {
					$diff = $frame.find('table.diff');
					if ($diff.length) {
						mw.hook('wikipage.diff').fire($diff.eq(0));
					}
					// Only scroll up if the user scrolled down
					// Leave scroll offset unchanged otherwise
					scrollIntoViewIfNeeded($frame);
				}
			});

			e.preventDefault();
		});

		$feed.on('click', 'a.newPage', function (e) {
			var $item = $(this).closest('.mw-rtrc-item').addClass('mw-rtrc-item-current'),
				title = $item.find('.page').text(),
				href = $item.find('.page').attr('href'),
				$frame = $('#krRTRC_DiffFrame');

			$feed.find('.mw-rtrc-item-current').not($item).removeClass('mw-rtrc-item-current');

			currentDiffRcid = Number($item.data('rcid'));

			$frame
				.addClass('mw-rtrc-diff-loading mw-rtrc-diff-newpage')
				.removeClass('mw-rtrc-diff-closed');

			$.ajax({
				url: href,
				dataType: 'html',
				data: {
					action: 'render',
					uselang: conf.wgUserLanguage
				}
			}).fail(function (jqXhr) {
				$frame
					.append(jqXhr.responseText || 'Loading diff failed.')
					.removeClass('mw-rtrc-diff-loading');
			}).done(function (data) {
				var skipButtonHtml;
				if ($.inArray(currentDiffRcid, skippedRCIDs) !== -1) {
					skipButtonHtml = '<span class="tab"><a id="diffUnskip">' + message('unskip').escaped() + '</a></span>';
				} else {
					skipButtonHtml = '<span class="tab"><a id="diffSkip">' + message('skip').escaped() + '</a></span>';
				}

				$frame
					.html(data)
					.prepend(
						'<h3>' + title + '</h3>' +
						'<div class="mw-rtrc-diff-tools">' +
							'<span class="tab"><a id="diffClose">X</a></span>' +
							'<span class="tab"><a href="' + href + '" target="_blank" id="diffNewWindow">Open in Wiki</a></span>' +
							'<span class="tab"><a onclick="$(\'.patrollink a\').click()">[mark]</a></span>' +
							'<span class="tab"><a id="diffNext">' + mw.message('next').escaped() + ' &raquo;</a></span>' +
							skipButtonHtml +
						'</div>'
					)
					.removeClass('mw-rtrc-diff-loading');

				if (opt.app.massPatrol) {
					$frame.find('.patrollink a').click();
				}
			});

			e.preventDefault();
		});

		// Mark as patrolled
		$wrapper.on('click', '.patrollink', function () {
			var $el = $(this);
			$el.find('a').text(mw.msg('markaspatrolleddiff') + '...');
			api.postWithToken('patrol', {
				action: 'patrol',
				rcid: currentDiffRcid
			}).done(function (data) {
				if (!data || data.error) {
					$el.empty().append(
						$('<span style="color: red;"></span>').text(mw.msg('markedaspatrollederror'))
					);
					mw.log('Patrol error:', data);
					return;
				}
				$el.empty().append(
					$('<span style="color: green;"></span>').text(mw.msg('markedaspatrolled'))
				);
				$feed.find('.mw-rtrc-item[data-rcid="' + currentDiffRcid + '"]').addClass('mw-rtrc-item-patrolled');

				// Feed refreshes may overlap with patrol actions, which can cause patrolled edits
				// to show up in an "Unpatrolled only" feed. This is make nextDiff() skip those.
				annotationsCacheUp();
				annotationsCache.patrolled[currentDiffRcid] = true;

				if (opt.app.autoDiff) {
					nextDiff();
				}
			}).fail(function () {
				$el.empty().append(
					$('<span style="color: red;"></span>').text(mw.msg('markedaspatrollederror'))
				);
			});

			return false;
		});

		// Trigger NextDiff
		$wrapper.on('click', '#diffNext', function () {
			nextDiff();
		});

		// SkipDiff
		$wrapper.on('click', '#diffSkip', function () {
			$feed.find('.mw-rtrc-item[data-rcid="' + currentDiffRcid + '"]').addClass('mw-rtrc-item-skipped');
			// Add to array, to re-add class after refresh
			skippedRCIDs.push(currentDiffRcid);
			nextDiff();
		});

		// UnskipDiff
		$wrapper.on('click', '#diffUnskip', function () {
			$feed.find('.mw-rtrc-item[data-rcid="' + currentDiffRcid + '"]').removeClass('mw-rtrc-item-skipped');
			// Remove from array, to no longer re-add class after refresh
			skippedRCIDs.splice(skippedRCIDs.indexOf(currentDiffRcid), 1);
		});

		// Show helpicons
		$('#mw-rtrc-toggleHelp').click(function (e) {
			e.preventDefault();
			$('#krRTRC_RCOptions').toggleClass('mw-rtrc-nohelp mw-rtrc-help');
		});

		// Link helpicons
		$('.mw-rtrc-settings .helpicon')
			.attr('title', msg('helpicon-tooltip'))
			.click(function (e) {
				e.preventDefault();
				window.open(docUrl + '#' + $(this).attr('section'), '_blank');
			});

		// Mark as patrolled when rollbacking
		// Note: As of MediaWiki r(unknown) rollbacking does already automatically patrol all reverted revisions.
		// But by doing it anyway it saves a click for the AutoDiff-users
		$wrapper.on('click', '.mw-rollback-link a', function () {
			$('.patrollink a').click();
		});

		// Button: Pause
		$('#rc-options-pause').click(function () {
			if (!this.checked) {
				// Unpause
				updateFeedNow();
				return;
			}
			clearTimeout(updateFeedTimeout);
		});
	}

	function showUnsupported() {
		$('#content').empty().append(
			$('<p>').addClass('errorbox').text(
				'This program requires functionality not supported in this browser.'
			)
		);
	}

	/**
	 * @param {string} [errMsg]
	 */
	function showFail(errMsg) {
		$('#content').empty().append(
			$('<p>').addClass('errorbox').text(errMsg || 'An unexpected error occurred.')
		);
	}

	/**
	 * Init functions
	 * -------------------------------------------------
	 */

	/**
	 * Fetches all external data we need.
	 *
	 * This runs in parallel with loading of modules and i18n.
	 *
	 * @return {jQuery.Promise}
	 */
	function initData() {
		var promises = [];

		// Get userrights
		promises.push(
			mw.loader.using('mediawiki.user').then(function () {
				return mw.user.getRights().then(function (rights) {
					if ($.inArray('patrol', rights) !== -1) {
						userHasPatrolRight = true;
					}
				});
			})
		);

		// Get MediaWiki interface messages
		promises.push(
			mw.loader.using('mediawiki.api.messages').then(function () {
				return new mw.Api().loadMessages([
					'blanknamespace',
					'contributions',
					'contribslink',
					'diff',
					'markaspatrolleddiff',
					'markedaspatrolled',
					'markedaspatrollederror',
					'namespaces',
					'namespacesall',
					'next',
					'talkpagelinktext'
				]);
			})
		);

		promises.push($.ajax({
			url: apiUrl,
			dataType: 'json',
			data: {
				format: 'json',
				action: 'query',
				list: 'tags',
				tgprop: 'displayname'
			}
		}).then(function (data) {
			var tags = data.query && data.query.tags;
			if (tags) {
				rcTags = $.map(tags, function (tag) {
					return tag.name;
				});
			}
		}));

		promises.push($.ajax({
			url: apiUrl,
			dataType: 'json',
			data: {
				format: 'json',
				action: 'query',
				meta: 'siteinfo'
			}
		}).then(function (data) {
			wikiTimeOffset = (data.query && data.query.general.timeoffset) || 0;
		}));

		return $.when.apply(null, promises);
	}

	/**
	 * @return {jQuery.Promise}
	 */
	function init() {
		var dModules, dI18N, featureTest, $navToggle, dOres;

		// Transform title and navigation tabs
		document.title = 'RTRC: ' + conf.wgDBname;
		$(function () {
			$('#p-namespaces ul')
				.find('li.selected')
					.removeClass('new')
					.find('a')
						.text('RTRC');
		});

		featureTest = !!(Date.parse);

		if (!featureTest) {
			$(showUnsupported);
			return;
		}

		// These selectors from vector-hd conflict with mw-rtrc-available
		$('.vector-animateLayout').removeClass('vector-animateLayout');

		$('html').addClass('mw-rtrc-available');

		if (navSupported) {
			$('html').addClass('mw-rtrc-sidebar-toggleable');
			$(function () {
				$navToggle = $('<div>').addClass('mw-rtrc-navtoggle');
				$('body').append($('<div>').addClass('mw-rtrc-sidebar-cover'));
				$('#mw-panel')
					.append($navToggle)
					.hover(function () {
						$('html').addClass('mw-rtrc-sidebar-on');
					}, function () {
						$('html').removeClass('mw-rtrc-sidebar-on');
					});
			});
		}

		dModules = mw.loader.using([
			'json',
			'jquery.client',
			'mediawiki.action.history.diff',
			// mw-plusminus styles etc.
			'mediawiki.special.changeslist',
			'mediawiki.jqueryMsg',
			'mediawiki.Uri',
			'mediawiki.user',
			'mediawiki.util',
			'mediawiki.api',
			'mediawiki.api.messages'
		]);

		if (!mw.libs.getIntuition) {
			mw.libs.getIntuition = $.ajax({ url: intuitionLoadUrl, dataType: 'script', cache: true, timeout: 7000 /*ms*/ });
		}

		dOres = $.ajax({
			url: oresApiUrl,
			dataType: $.support.cors ? 'json' : 'jsonp',
			cache: true,
			timeout: 2000
		}).then(function (data) {
			if (data && data.models) {
				if (data.models.damaging) {
					oresModel = 'damaging';
				} else if (data.models.reverted) {
					oresModel = 'reverted';
				}
			}
		}, function () {
			// If ORES doesn't have models for this wiki, do continue loading without
			return $.Deferred().resolve();
		});

		dI18N = mw.libs.getIntuition
			.then(function () {
				return mw.libs.intuition.load('rtrc');
			})
			.then(function () {
				message = $.proxy(mw.libs.intuition.message, null, 'rtrc');
				msg = $.proxy(mw.libs.intuition.msg, null, 'rtrc');
			}, function () {
				// Ignore failure. RTRC should load even if Labs is down.
				// Fallback to displaying message keys.
				mw.messages.set('intuition-i18n-gone', '$1');
				message = function (key) {
					return mw.message('intuition-i18n-gone', key);
				};
				msg = function (key) {
					return key;
				};
				return $.Deferred().resolve();
			});

		$.when(initData(), dModules, dI18N, dOres, $.ready).fail(showFail).done(function () {
			if ($navToggle) {
				$navToggle.attr('title', msg('navtoggle-tooltip'));
			}

			// Map over months
			monthNames = msg('months').split(',');

			buildInterface();
			readPermalink();
			updateFeedNow();

			scrollIntoView($wrapper);
			rAF(function () {
				$('html').addClass('mw-rtrc-ready');
			});

			bindInterface();
		});
	}

	/**
	 * Execution
	 * -------------------------------------------------
	 */

	// On every page
	$(function () {
		if (!$('#t-rtrc').length) {
			mw.loader.using('mediawiki.util', function () {
				mw.util.addPortletLink(
					'p-tb',
					mw.util.getUrl('Special:BlankPage/RTRC'),
					'RTRC',
					't-rtrc',
					'Monitor and patrol recent changes in real-time',
					null,
					'#t-specialpages'
				);
			});
		}
	});

	// Initialise if in the right context
	if (
		(conf.wgTitle === 'Krinkle/RTRC' && conf.wgAction === 'view') ||
		(conf.wgCanonicalSpecialPageName === 'Blankpage' && conf.wgTitle.split('/', 2)[1] === 'RTRC')
	) {
		init();
	}

}(jQuery, mediaWiki));
Afkomstig van Wikikids , de interactieve Nederlandstalige Internet-encyclopedie voor en door kinderen. "https://wikikids.nl/index.php?title=Gebruiker:Xxmarijnw/RTRC.js&oldid=454516"