Jump to content

User:Drewmutt/RTRC.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
importStylesheet( 'User:Drewmutt/RTRC.css' );
/**
 * Drewmutt hack times!
 * 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 = '/media/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="mw-title" 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.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 results = data.query.recentchanges;
                    var newResults = [];

                    console.log("DATATATA:", results);

                    for(var i = 0; i < results.length; i++)
                    {
                        var result = results[i];
                        var editSummary = result.parsedcomment;
                        var isRedirect = editSummary.toLowerCase().indexOf("direct") !== -1;
                        console.log(editSummary);
                        console.log(isRedirect);
                        if(!isRedirect)
                        {
                            newResults.push(result);
                        }
                    }

                    console.log("New Results:", newResults);
                    data.query.recentchanges = newResults;

                    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() + ' &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('.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() + ' &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.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 === 'Drewmutt/RTRC' && conf.wgAction === 'view') ||
            (conf.wgCanonicalSpecialPageName === 'Blankpage' && conf.wgTitle.split('/', 2)[1] === 'RTRC')
    )
    {
        console.log("We good!");
        init();
    }
    else
    {
        console.log("errrrrr!");
    }

}(jQuery, mediaWiki));