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.
/*
Last update: February 13, 2019
Status: Experimental
*/
(function() {
  var config = {
    timer: null,
    status: false
  };
  var token = false,
    current_revisions = {},
    last_state = {};

  function is_sandbox() {
    if ($("div#watchlist-ajax")) {
      return true;
    } else {
      return false;
    }
  }
  if (is_sandbox()) {
    let top_content = "<input id='ajax_watchlist_timeout' placeholder='Refresh rate in milliseconds'><button id='ajax_watchlist_start'>Start</button><button id='ajax_watchlist_stop'>Stop</button><br>Status: <span id='ajax_watchlist_status'>not running</span>";
    let main_content = "<div id='auto_updating_watchlist'></div>";
    $("div#watchlist-ajax").html(top_content + main_content);
    $("#ajax_watchlist_start").click(function() {
      start_updating();
    });
    $("#ajax_watchlist_stop").click(function() {
      stop_updating();
    });
  }

  function start_updating() {
    if (is_sandbox() && !config.status) {
      var timeout = $.isNumeric($("#ajax_watchlist_timeout").val()) ? $("#ajax_watchlist_timeout").val() : 5000;
      config.status = true;
      $("#auto_updating_watchlist").html("");
      update_watchlist(timeout);
      $("#ajax_watchlist_status").html(`running (${timeout} ms)`);
    }
  }

  function stop_updating() {
    config.status = false;
    clearTimeout(config.timer);
    $("#ajax_watchlist_status").html('stopped');
  }

  function update_watchlist(timeout) {
    if (is_sandbox() && config.status) {
      $.ajax({
        type: 'GET',
        url: '/w/api.php?action=query&list=watchlist&wllimit=30&wlprop=ids|title|sizes|flags|user|comment&format=json&curtimestamp=true&wlallrev=true',
        dataType: 'json',
        loop_timeout: timeout,
        success: function(returndata) {
          let sitelink = ''; /* for local testing */
          let watchlist_items = "";
          let fetched_revs = [];
          returndata = returndata.query.watchlist;
          if (returndata.length > 0) {
            returndata.forEach(function(e) {
              fetched_revs.push(e.pageid);
              let length = e.newlen - e.oldlen;
              let edit_summary = e.comment.substring(0, 400) || "<span style='color:#cecccc'>No edit summary.</span>" /* trim extremely long edit summary*/ ;
              edit_summary = edit_summary.replace(/\[\[(.*?)\|(.*?)\]\]/g, "<a href=\"/wiki/$1\">$2</a>"); /* parse wikilinks */
              let difflink = sitelink + `/w/index.php?title=${e.title}&diff=${e.revid}`;
              let histlink = sitelink + `/w/index.php?title=${e.title}&action=history`;
              let rollback_msg = "ajax rollback";
              if (e.revid in last_state) {
                rollback_msg = last_state[e.revid];
              }
              let rollback_btn = `[<a href='#' class='ajax_rollback_button' data-rollback-pageid="${e.pageid}"  data-rollback-revid="${e.revid}" data-rollback-user="${e.user}">${rollback_msg}</a>]`;
              if (e.pageid in current_revisions) {
                if (current_revisions[e.pageid] != e.revid) {
                  rollback_btn = "";
                }
              }

              let userlink = `
							<a href='${sitelink}/wiki/User:${e.user}'>${e.user}</a>
							(<a href='${sitelink}/wiki/User talk:${e.user}'>talk</a> | 
							<a href='${sitelink}/wiki/Special:Contributions/${e.user}'>contrib</a>)
							`;
              length = (length < 0 ? '<span style=color:red>' + length + '</span>' : '<span style=color:green>+' + length + '</span>');
              watchlist_items += `
							<div style='margin-bottom:5px; padding:3px; font-family:Calibri; width: 100%; word-break:break-all'>
							(<a href="${difflink}">diff</a> | <a href="${histlink}">hist</a>)
							<a href="${sitelink}/wiki/${e.title}">${e.title}</a> (${e.type}) (${length}) &mdash; ${userlink} (<span style='font-style:italic'>${edit_summary}</span>) <span class="mw-rollback-link"  data-rollback-pageid="${e.pageid}" data-rollback-user="${e.user}" data-revid="${e.revid}">${rollback_btn}</span>
							</div>
							`;
            });
            $('.b-border-div').remove();
            $("#auto_updating_watchlist").html(watchlist_items + "<div style='border-bottom:2px solid skyblue;' class='b-border-div'></div>");
            $(".ajax_rollback_button").click(function(e) {
              custom_rollback(e, this);
            });
          }
          check_revisions(fetched_revs);
        },
        error: e => console.log(e),
        complete: function() {
          var timeout = this.loop_timeout;
          config.timer = setTimeout(function() {
            update_watchlist(timeout);
          }, timeout);
        }
      });
    }
  }


  function check_revisions(pageid_array) {
    pageid_array = [...new Set(pageid_array)];
    var pageids = pageid_array.join("|");
    $.get("/w/api.php?action=query&format=json&prop=revisions&pageids=" + pageids).then(response => {
      var pages = response.query.pages;
      pageid_array.forEach(pageid => {
      	var revision = "";
      	if(pages[pageid].revisions === undefined){
      		revision = '123'; /* probably a block log */        	
      	}else{
      		revision = pages[pageid].revisions[0].revid;
      	}
        current_revisions[pageid] = revision;
        $(`.mw-rollback-link[data-rollback-pageid=${pageid}]`).each(function() {
          if ($(this)[0].dataset.revid != revision) {
            $(this).html("");
          } else {
            let pageid = $(this)[0].dataset.rollbackPageid;
            let user = $(this)[0].dataset.rollbackUser;
            let rollback_msg = "ajax rollback";
            if (revision in last_state) {
              rollback_msg = last_state[revision];
            }
            let rollback_btn = `[<a href='#' class='ajax_rollback_button' data-rollback-pageid="${pageid}"  data-rollback-revid="${revision}" data-rollback-user="${user}">${rollback_msg}</a>]`;
            $(this).html(rollback_btn);
          }
        });
      });
      $(".ajax_rollback_button").click(function(e) {
        custom_rollback(e, this);
      });
    });
  }

  function getToken(first_par, second_par) {
    if (!token) {
      $.ajax("/w/api.php?action=query&meta=tokens&type=rollback&format=json").done(data => {
        token = data.query.tokens.rollbacktoken;
        custom_rollback(first_par, second_par, true);
      });
    } else {
      custom_rollback(first_par, second_par, true);
    }
  }

  function custom_rollback(e, clicked, withToken) {
    e.preventDefault();
    if ($(clicked).attr('href') === null) {
      return;
    }
    if (!withToken) {
      $(clicked).text("getting token");
      getToken(e, clicked);
      return;
    }
    var ID = $(clicked).attr("data-rollback-pageid");
    var user = $(clicked).attr("data-rollback-user");
    $(clicked).text('rollbacking...');
    last_state[$(clicked)[0].dataset.rollbackRevid] = $(clicked).text();
    $.ajax({
      type: 'POST',
      url: '/w/api.php',
      c_b: clicked,
      data: {
        action: "rollback",
        pageid: ID,
        user: user,
        token: token,
        format: 'json'
      },
      success: function(e) {
        if (!e.error) {
          $(this.c_b).text('reverted');
        } else {
          $(this.c_b).text(e.error.code);
          console.log(e);
        }
        last_state[$(this.c_b)[0].dataset.rollbackRevid] = $(this.c_b).text();
      },
      error: function(e) {
        $(this.c_b).text('rollback failed');
        last_state[$(this.c_b)[0].dataset.rollbackRevid] = $(this.c_b).text();
        console.log(e);
      }
    });
  }
  mw.util.addPortletLink('p-cactions', '/wiki/User:RainFall/sandbox', 'Ajax watchlist');
})();