User:Drewmutt/yourscript.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Drewmutt/yourscript. |
/**
* 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.3.2',
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 = ' ';
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> <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="mw-title" href="' + mw.util.getUrl(rc.title) + '?rcid=' + rc.rcid + '" target="_blank">' + rc.title + '</a>' +
'</div>' +
'<div user> <small>· ' +
'<a href="' + mw.util.getUrl('User talk:' + rc.user) + '" target="_blank">' + mw.message('talkpagelinktext').escaped() + '</a>' +
' · ' +
'<a href="' + mw.util.getUrl('Special:Contributions/' + rc.user) + '" target="_blank">' + mw.message('contribslink').escaped() + '</a>' +
' </small>· ' +
'<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> <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.rcexcludeuser = 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('.mw-title').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() + ' »</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('.mw-title').text(),
href = $item.find('.mw-title').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() + ' »</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.diff.styles',
// 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
$.when(mw.loader.using('mediawiki.util'), $.ready).then(function ()
{
if (!$('#t-rtrc').length)
{
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'
);
}
if (conf.wgCanonicalSpecialPageName === 'Recentchanges' && !$('#ca-nstab-rtrc').length)
{
mw.util.addPortletLink(
'p-namespaces',
mw.util.getUrl('Special:BlankPage/RTRC'),
'RTRC',
'ca-nstab-rtrc',
'Monitor and patrol recent changes in real-time'
);
}
});
// 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));