Jump to content

User:Ponor/inline-diff-inline-patrol.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*
Inline [diff] for revisions in RECENT CHANGES, WATCHLIST, PAGE HISTORY and USER CONTRIBUTIONS.
Adds [patrol] buttons for unpatrolled edits for users with sysop or patrol rights.
Inspired by Bradv's Expand Diffs, Writ Keeper's Common History, and Ivi1o4's Unpatrolled User Contributions scripts.
Author:Ponor (2024-06-10)
Documentation: meta.wikimedia.org/wiki/User:Ponor/inline-diff-inline-patrol
*/
$(document).ready(function() {

"use strict";

  //user groups
  const patroller = mw.config.get("wgUserGroups").includes("patroller");
  const sysop = mw.config.get("wgUserGroups").includes("sysop");

  //which page are we on?
  const RC = mw.config.get("wgCanonicalSpecialPageName") == "Recentchanges";
  const UC = mw.config.get("wgCanonicalSpecialPageName") == "Contributions";
  const WL = mw.config.get("wgCanonicalSpecialPageName") == "Watchlist";
  const PH = mw.config.get("wgAction") == "history"; //Page History

  if (window.inline_patrol_running ||
    //!(patroller || sysop) ||
    !(RC || UC || PH || WL)
  ) {
    return;
  } //get out of here

  //------------------------------------ Get and set styles
  //diff_styles: mw.loader.load('mediawiki.diff.styles'),
  mw.loader.load('https://www.mediawiki.org/w/load.php?modules=mediawiki.diff.styles&only=styles', 'text/css');
  mw.loader.load('https://meta.wikimedia.org/wiki/User:Ponor/inline-diff-inline-patrol.css?action=raw&ctype=text/css', 'text/css');

  let conf = {
    patrol_button: "patrol",
    patrol_button_hint: "mark as patrolled: revision #",
    patrol_successful: "Patrolling revision #", //pop-up notification
    patrol_error: "Error patrolling revision #", //pop-up notification
    diff_button: "diff",
    diff_button_hint: "show diff",
    diff_type: "table", //inline, table
    max_diff_height: "40em",
  }; //conf
  
  //------------------------------------ MAIN
  function on_ready() {
    //the script should not be loaded twice
    window.inline_patrol_running = true;

    if (window.ip_configuration) {
      for (const c in conf) {
        conf[c] = window.ip_configuration[c] || conf[c];
      }
    }

    //Finally
    function initialize() {
      if (patroller || sysop) {
        $("li[data-mw-revid].mw-changeslist-reviewstatus-unpatrolled").each(patrolButton);
      }
      $("li[data-mw-revid]").each(diffButton);
    } //function initialize

    //Recent Changes and Watchlist
    if (RC || WL) {
      initialize();
      //mw.hook because RC page likes to update itself
      mw.hook("wikipage.content").add(function(el) {
        if (el.hasClass('mw-changeslist')) {
          initialize();
        }
      });
    }

    //User Contributions and Page History
    if (UC || PH) {
      get_unpatrolled();
      mw.hook('userjs.inline_patrol.get_patrol_data').add(function(status) {
        initialize();
      });
    }
  } //function on_ready

  //------------------------------------ Buttons
  function diffButton() {
    let revid = $(this).attr("data-mw-revid");
    //console.log(revid)
    if (revid) {
      let button = $('<span/>');
      button.addClass("ip-button ip-diff")
        .text(conf.diff_button + "↓")
        .attr("title", conf.diff_button_hint)
        .data("revid", revid)
        .on("click", diffClick);
      //button.insertAfter($(this).find("span.mw-changeslist-separator").eq(0));
      //button.prependTo($(this));
      if (RC || WL) {
        //button.prependTo($("span.mw-changeslist-line-inner", $(this)));
        button.insertBefore($("span.mw-changeslist-line-inner", $(this)));
      } else if (PH || UC) {
        button.prependTo($(this));
      }
    }
  } //function diffButton
  
  function patrolButton() {
    let revid = $(this).attr("data-mw-revid");
    if (revid) {
      //console.log(revid)
      let button = $('<span/>');
      button.addClass("ip-button ip-unpatrolled")
        .text(conf.patrol_button)
        .attr("title", conf.patrol_button_hint + revid)
        .data("revid", revid)
        .on("click", patrolClick);
      //button.insertAfter($(this).find("span.mw-changeslist-separator").eq(0));
      //button.insertAfter($(this).find(".mw-changeslist-date").eq(0));
      if (RC || WL) {
        button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(1));
      } else if (PH || UC) {
        button.insertAfter($("span.mw-changeslist-separator", $(this)).eq(0));
      }
    }
  } //function patrolButton
  
  //------------------------------------ mw.API calls 
  //mw.API patrol call
  function patrol(revid) {
    console.log("(inline-patrol) patrolling revid ", revid);
    let mwPromise = new mw.Api().postWithToken('patrol', {
      'action': 'patrol',
      'format': 'json',
      'revid': revid
    });
    return mwPromise;
  }

  //mw.API diff call
  function get_diff(revid) {
    console.log("(inline-patrol) getting diff for ", revid);
    let mwPromise = new mw.Api().get({
      'action': 'compare',
      'format': 'json',
      'uselang': 'content', 'maxage': '7200', 'smaxage': '7200', //cache results for 2h
      'difftype': conf.diff_type, //inline, table, unified
      'torelative': 'prev',
      'fromrev': revid
    });
    return mwPromise;
  } //get_diff

  //mw.API call for unpatrolled edits on Page History and User Contribution pages
  function get_unpatrolled() {
    let rcKey, rcValue;
    if (UC) {
      rcKey = "rcuser";
      rcValue = mw.config.get("wgRelevantUserName");
    }

    if (PH) {
      rcKey = "rctitle";
      rcValue = mw.config.get("wgPageName");
    }

    if (UC || PH) {
      let unpatrolledRevisionsPromise = new mw.Api().get({
        "action": "query",
        "format": "json",
        "list": "recentchanges",
        "rcprop": "ids|patrolled",
        "rcshow": "unpatrolled",
        "rclimit": "123",
        [rcKey]: rcValue //mdn: computed property names
      });

      unpatrolledRevisionsPromise.done(function(jsondata) {
        $.each(jsondata.query.recentchanges, function(i, rc) {
          $("li[data-mw-revid=" + rc.revid + "]").addClass("mw-changeslist-reviewstatus-unpatrolled"); //same as Recent Changes <li's>
        });
        mw.hook('userjs.inline_patrol.get_patrol_data').fire("done");
      });

      unpatrolledRevisionsPromise.fail(function(jsondata) {
        mw.hook('userjs.inline_patrol.get_patrol_data').fire("failed");
      });
    }
  } //function get_patrolled

  //------------------------------------ Button click handlers
  //[patrol] button click handler
  function patrolClick(event) {
    let button = $(this);
    const revid = button.data().revid;

    const mwPromise = patrol(revid);
    mwPromise.done((data) => { //arrow function does not create its context, "this" is from outer context
      mw.notify(conf.patrol_successful + revid, {'type':'success'});
      button
        .removeClass("ip-unpatrolled").addClass("ip-patrolled")
        .off(); //no clicks
    });

    mwPromise.fail((data) => { //arrow function does not create its context, "this" is from outer context
      mw.notify(conf.patrol_error + revid, {'type':'error'});
      button
        .removeClass("ip-unpatrolled").addClass("ip-error")
        .off(); //no clicks
    });

    event.stopPropagation();
  } //function patrolClick

  //[diff] button click handler
  function diffClick(event) {
    let button = $(this);
    let parent = button.parent();

    if (button.data("status") == "shown") {
      parent.children("div.ip-diff-div").eq(0).hide();
      button.text(conf.diff_button + "↓")
            .data("status", "hidden");
      return;
    } else if (button.data("status") == "hidden") {
      parent.children("div.ip-diff-div").eq(0).show();
      button.text(conf.diff_button + "↑")
            .data("status", "shown");
      return;
    }

    function diff_promise_done(data) {
      let diff_data = data.compare["*"];
      
      //linkify
      function proper_nesting(m, p1, p2, p3, link_class, prefix='') {
        let link = p2;
        let s_start = p1;
        if (p2.search(/^[^⥅⥆⥴⥳]+?⥆/)>=0) {s_start = s_start+"⥆"; link = "⥅"+link;}
        if (p2.search(/^[^⥅⥆⥴⥳]+?⥳/)>=0) {s_start = s_start+"⥳"; link = "⥴"+link;}
        let s_end = p3;
        if (p2.search(/⥅[^⥅⥆⥴⥳]+$/)>=0) {s_end = "⥅"+s_end; link = link+"⥆";}
        if (p2.search(/⥴[^⥅⥆⥴⥳]+$/)>=0) {s_end = "⥴"+s_end; link = link+"⥳";}
        return s_start + '<a class="' +link_class + '"target="_blank" href="/wiki/' + prefix + p2.replaceAll(/[⥅⥆⥴⥳]/g, '').replaceAll(' ', '_') + '">' + link + '</a>' + s_end;
      } //proper_nesting
      function linked_wl(m, p1, p2, p3) {return proper_nesting(m, p1, p2, p3, "ip-diff-wl", "");}
      function linked_tl(m, p1, p2, p3) {return proper_nesting(m, p1, p2, p3, "ip-diff-tl", "Template:");}
      function linked_url(m) {return '<a class="ip-diff-url" href="' + m.replaceAll(/[⥅⥆⥴⥳]/g, '') + '">' + m + '</a>';}
      
      if (conf.diff_type == 'table') {
        let del_tag = '<del class="diffchange diffchange-inline">';
        let ins_tag = '<ins class="diffchange diffchange-inline">';

        diff_data = diff_data
          .replaceAll(del_tag, '⥴').replaceAll('</del>', '⥳')
          .replaceAll(ins_tag, '⥅').replaceAll('</ins>', '⥆')
          .replaceAll(/https?:\/\/[^\|\}\]<>\s]+/g, linked_url)
          .replaceAll(/(\[\[ *[⥅⥆⥴⥳]? *)([^\]\|]+?)( *[⥅⥆⥴⥳]? *[\]\|])/g, linked_wl)
          .replaceAll(/(?<!\{)(\{\{(?![#{]) *[⥅⥆⥴⥳]? *)([^\}\|\n<]+?)( *[⥅⥆⥴⥳]? *[\}\|\n<])/g, linked_tl)
          .replaceAll("⥅⥆","").replaceAll("⥅⥆","⥴⥳")
          .replaceAll('⥴', del_tag).replaceAll('⥳', '</del>')
          .replaceAll('⥅', ins_tag).replaceAll('⥆', '</ins>');
      }
      
      let diff_table = `<table class="diff">\n<colgroup>\n  <col class="diff-marker">\n  <col class="diff-content">\n  <col class="diff-marker">\n  <col class="diff-content">\n</colgroup>\n<tbody>\n${diff_data}\n</tbody>\n</table>`;
      let diff_div = $('<div/>');
      diff_div
        .addClass("ip-diff-div")
        .css({"max-height": conf.max_diff_height, "overflow-y": "auto"})
        .html(diff_table)
        .on("dblclick", function() {
          $(this).hide();
          button.data("status", "hidden");
        });
      parent.append(diff_div);
      button.data("status", "shown")
            .text(conf.diff_button + "↑");
    } //diff_promise_done

    const revid = button.data().revid;
    const mwPromise = get_diff(revid);

    mwPromise.done(diff_promise_done);
    event.stopPropagation();
  }


on_ready();

});