User:SD0001/W-Ping.js

//

$.when(

mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.user',

'ext.gadget.morebits', 'mediawiki.widgets.DateInputWidget', 'moment']),

$.ready

).then(function() {

var WPing = {};

window.WPing = WPing;

WPing.api = new mw.Api({

parameters: { formatversion: 2 },

ajax: { headers: { 'Api-User-Agent': 'w:en:User:SD0001/W-Ping.js' } }

});

WPing.pingDialog = function pingDialog(page) {

var Window = new Morebits.simpleWindow(800, 500);

Window.setScriptName('W-Ping');

Window.setTitle("Schedule a watchlist ping for " + page);

Window.addFooterLink('Upcoming pings', 'Special:BlankPage/W-Ping');

Window.addFooterLink('W-Ping', 'User:SD0001/W-Ping');

var form = new Morebits.quickForm(WPing.evaluate);

var reason = '';

var date = moment().utcOffset(WPing.getUserTimeZone()).format('YYYY-MM-DD');

// See if there's already a scheduled ping for this page, if so override the above defaults

var opt = JSON.parse(mw.user.options.get('userjs-wping-list'));

if (opt && opt[page]) {

reason = opt[page][1];

date = moment(opt[page][0] * 60000).utcOffset(WPing.getUserTimeZone()).format('YYYY-MM-DD');

}

form.append({

type: 'input',

label: 'Reason: ',

name: 'reason',

value: reason,

size: '100px'

});

// input field replaced by datepicker after render

form.append({

type: 'input',

name: 'date',

label: 'Ping on: ',

});

form.append({

type: 'hidden',

name: 'page',

value: page

});

if (opt && opt[page] && mw.config.get('wgCanonicalSpecialPageName') !== 'Watchlist') {

form.append({

type: 'button',

label: 'Cancel ping',

style: 'margin-top: 5px',

event: function cancelPing() {

Morebits.status.init(result);

Morebits.simpleWindow.setButtonsEnabled(false);

var status = new Morebits.status('Ping', 'Cancelling', 'status');

WPing.retrievePingList().then(pings => {

delete pings[page];

WPing.updatePingList(pings).then(() => {

status.info('Done');

mw.track('counter.gadget_WPing.ping_cancelled');

mw.track('stats.mediawiki_gadget_test_wping_total', 1, { action: 'ping_cancelled' });

window.setTimeout(() => {

Window.close(); // close dialog

}, 300);

}).catch(err => {

status.error('Failed to cancel: ' + JSON.stringify(err));

});

}).catch(err => {

status.error('Failed to retrieve pings list: ' + JSON.stringify(err));

});

}

});

}

form.append({ type: 'submit', label: 'Submit' });

var result = form.render();

Window.setContent(result);

Window.display();

mw.track('counter.gadget_WPing.dialog_opened');

mw.track('stats.mediawiki_gadget_test_wping_total', 1, { action: 'dialog_opened' });

var datepicker = new mw.widgets.DateInputWidget({

name: 'date',

value: date

});

datepicker.setRequired(true);

$(result.date).replaceWith(datepicker.$element);

// prevent datepicker from getting hidden into the dialog

$(Window.content).parent().css('overflow', 'visible');

$(Window.content).css('overflow', 'visible');

datepicker.$element.find('label').css({

'display': 'block',

'font-size': '110%'

});

// prevent enter in date field from submitting

// leads to surprises if date was invalid, as datepicker takes in a close valid date anyway

datepicker.$element.find('input[type=text]').keypress(e => {

if (e.keyCode === 13) {

e.preventDefault();

return false;

}

});

var durations = Array.isArray(window.WPing_Quick_Durations) ?

window.WPing_Quick_Durations :

[ '1 day', '3 days', '1 week', '2 weeks', '1 month' ];

var $quickSelect = $('');

durations.forEach(e => {

$('').addClass('wping-prompt').text(e).appendTo($quickSelect);

});

datepicker.$element.after($quickSelect);

$quickSelect.find('a').css({

'padding': '0 5px 0 5px'

}).click(e => {

e.preventDefault();

// moment doesn't natively parse durations such as "3 weeks", so we manually separate

// the number and the unit, and give it as moment.duration(3, "weeks")

var s = e.target.textContent;

var i;

for (i = 0; i < s.length; i++) {

if (s[i] < '0' || s[i] > '9') {

break;

}

}

var num = parseInt(s);

var text = s.slice(i).trim();

var duration = moment.duration(num, text);

var targetdate = moment().add(duration);

datepicker.setValue(targetdate.utcOffset(WPing.getUserTimeZone()).format('YYYY-MM-DD'));

});

};

WPing.evaluate = function evaluate(e) {

var form = e.target;

var page = form.page.value;

var reason = form.reason.value;

// moment reads the date as if it is in the system time zone, we apply an offset correction

// to account for the case when it's differnt from the time zone in user preferences

var userzone = WPing.getUserTimeZone();

var syszone = -new Date().getTimezoneOffset();

var enteredDate = moment(form.date.value, 'YYYY-MM-DD').add(syszone - userzone, 'minutes');

// add hours and minutes to entered date:

var now = moment().utcOffset(WPing.getUserTimeZone());

var pingAt = enteredDate.add(now.hours(), 'hours').add(now.minutes(), 'minutes');

// store pingtime as number of minutes past unix epoch (to optimise storage)

var pingtime = parseInt(pingAt.unix() / 60);

Morebits.status.init(form);

Morebits.simpleWindow.setButtonsEnabled(false);

var status = new Morebits.status('Ping', 'Scheduling', 'status');

WPing.retrievePingList().then(pings => {

pings[page] = [ pingtime, reason ];

WPing.updatePingList(pings).then(() => {

status.info('Done');

mw.track('counter.gadget_WPing.ping_saved');

mw.track('stats.mediawiki_gadget_test_wping_total', 1, { action: 'ping_saved' });

// automatically close window in a short while

window.setTimeout(() => {

$(form).parent().prev().find('.ui-dialog-titlebar-close').click();

}, 300);

// while snoozing, remove the ping entry

if (mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist') {

WPing.removePingDisplayLine(page);

if (page !== Morebits.pageNameNorm) {

mw.track('counter.gadget_WPing.ping_snoozed');

mw.track('stats.mediawiki_gadget_test_wping_total', 1, { action: 'ping_snooze' });

}

}

}).catch(err => {

status.error('Failed: ' + JSON.stringify(err));

});

}).catch(err => {

status.error('Failed: ' + JSON.stringify(err));

});

};

WPing.attachPings = function attachPings() {

var opt = JSON.parse(mw.user.options.get('userjs-wping-list'));

if (!opt) return;

var $ul = $('

    ').css({

    'margin-left': 'calc((6px + 3px) * 5 + 0.35714286em)' // to match that of .mw-changeslist ul

    });

    var pingPages = [];

    $.each(opt, function(page, tr) {

    var pingtime = tr[0] * 60000;

    if (new Date().getTime() > pingtime) {

    pingPages.push(page);

    // render wikilinks in reason text, though all links will appear blue

    var reason = tr[1].replace(/\[\[:?(?:([^\|\]]+?)\|)?([^\]\|]+?)\]\]/g, (_, target, text) => {

    if (!target) {

    target = text;

    }

    return '' + text + '';

    });

    var histlink = mw.Title.newFromText(page).namespace < 0 ? 'hist' :

    ('hist');

    $('

  • ').addClass('wping-line').attr('data-page', page).html(

    '(' + histlink + ') ' +

    ' ' +

    '' + page + ' ' +

    ' ' +

    (reason ? '(' + reason + ') ' : '') +

    '[ snooze | dismiss ]'

    ).appendTo($ul);

    }

    });

    if (!pingPages.length) {

    return;

    }

    var $element = $('.mw-rcfilters-ui-changesListWrapperWidget').length ?

    $('.mw-rcfilters-ui-changesListWrapperWidget') :

    ( $('.mw-changeslist').length ? // for users of non-AJAX watchlist

    $('.mw-changeslist') :

    $('.mw-changeslist-empty') );

    $element.before(

    $('

    ').attr('id', 'wping').append(

    $('

    ').text('Pings'),

    $ul

    )

    );

    // check if pinged pages exists, if not turn the links red, occurs lazily

    for (let start = 0, end = 50; start < pingPages.length; start += 50, end += 50) {

    WPing.api.get({ titles: pingPages.slice(start, end) }).then(json => {

    json.query.pages.forEach(pg => {

    if (pg.missing) {

    $ul.find('a[href="' + mw.util.getUrl(pg.title) + '"]').addClass('new');

    }

    });

    });

    }

    $ul.find('.wping-snooze').click(e => {

    e.preventDefault();

    var page = $(e.target).parent().data('page');

    WPing.pingDialog(page);

    });

    $ul.find('.wping-dismiss').click(e => {

    e.preventDefault();

    var page = $(e.target).parent().data('page');

    WPing.retrievePingList().then(pings => {

    delete pings[page];

    WPing.updatePingList(pings);

    });

    WPing.removePingDisplayLine(page);

    mw.track('counter.gadget_WPing.ping_dismissed');

    mw.track('stats.mediawiki_gadget_test_wping_total', 1, { action: 'ping_dismissed' });

    });

    };

    // Retrive ping list using the API. Can't rely on mw.user.options

    // because it may be outdated due to changes in another tab.

    WPing.retrievePingList = function() {

    return WPing.api.get({

    meta: 'userinfo',

    uiprop: 'options'

    // XXX: API doesn't currently offer a way to retrieve a specific user option

    }).then(json => {

    return JSON.parse(json.query.userinfo.options['userjs-wping-list'] || '{}');

    });

    };

    WPing.updatePingList = function(opt) {

    var optString = JSON.stringify(opt);

    // update object locally too, so that it can be retrieved in case user wants to change reason/date

    // again (before page is reloaded)

    mw.user.options.set('userjs-wping-list', optString);

    return WPing.api.saveOption('userjs-wping-list', optString);

    };

    WPing.removePingDisplayLine = function removePingDisplayLine(page) {

    $('#wping ul li[data-page="' + $.escapeSelector(page) + '"]').remove();

    if ($('#wping ul').children().length === 0) {

    $('#wping').remove();

    }

    };

    WPing.buildSpecialPage = function buildSpecialPage() {

    $('#firstHeading').text('Upcoming watchlist pings');

    document.title = 'Upcoming watchlist pings';

    $('#mw-content-text').empty();

    var opt = JSON.parse(mw.user.options.get('userjs-wping-list'));

    if (!opt) {

    opt = {};

    }

    var timezone = WPing.getUserTimeZone();

    var $ul = $('

      ');

      $.each(opt, function(page, tr) {

      var time = new Date(tr[0] * 60000);

      // render wikilinks in reason text, though all links will appear blue

      var reason = tr[1].replace(/\[\[:?(?:([^\|\]]+?)\|)?([^\]\|]+?)\]\]/g, (_, target, text) => {

      if (!target) {

      target = text;

      }

      return '' + text + '';

      });

      $ul.append(

      $('

    • ').html(

      '' + page + ': ' +

      (reason ? '(' + reason + ') ' : '') +

      moment(time).utcOffset(timezone).format('HH:mm, D MMMM YYYY')

      )

      );

      });

      $('#mw-content-text').append(

      $('

      ').text('A ping shall be delivered to your watchlist for the following pages, at the specified time in ' + WPing.getTimeZoneString(timezone) + ' time zone:'),

      $ul

      );

      for (let start = 0, end = 50; start < Object.keys(opt).length; start += 50, end += 50) {

      WPing.api.get({ titles: Object.keys(opt).slice(start, end) }).then(json => {

      json.query.pages.forEach(pg => {

      if (pg.missing) {

      $ul.find('a[href="' + mw.util.getUrl(pg.title) + '"]').addClass('new');

      }

      });

      });

      }

      };

      WPing.getUserTimeZone = function() {

      if (WPing.userTimeZone) { // cache it

      return WPing.userTimeZone;

      }

      switch (window.WPing_timezone || 'preferences') {

      case 'utc':

      WPing.userTimeZone = 0;

      break;

      case 'system':

      WPing.userTimeZone = -new Date().getTimezoneOffset();

      break;

      case 'preferences':

      WPing.userTimeZone = parseInt(mw.user.options.get('timecorrection').split('|')[1]);

      break;

      }

      return WPing.userTimeZone;

      };

      WPing.getTimeZoneString = function(timecorrection) {

      var negative = false;

      if (timecorrection < 0) {

      timecorrection = -timecorrection;

      negative = true;

      }

      var hourCorrection = parseInt(timecorrection/60);

      hourCorrection = (hourCorrection < 10 ? '0' : '') + hourCorrection.toString();

      var minuteCorrection = timecorrection % 60;

      minuteCorrection = (minuteCorrection < 10 ? '0' : '') + minuteCorrection.toString();

      return 'UTC' + (negative ? '–' : '+') + hourCorrection + minuteCorrection;

      };

      // SET UP

      if (mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist') {

      WPing.attachPings();

      } else if (mw.config.get('wgPageName') === 'Special:BlankPage/W-Ping') {

      WPing.buildSpecialPage();

      } else {

      var pageName = Morebits.pageNameNorm;

      // for Special:Log views where the form at the top of the page was used:

      if (pageName === 'Special:Log') {

      var user = mw.util.getParamValue('user');

      var type = mw.util.getParamValue('type');

      if (type) {

      pageName += '/' + type;

      }

      if (user) {

      pageName += '/' + Morebits.string.toUpperCaseFirstChar(user);

      }

      } else if (pageName === 'Special:Contributions') {

      var user = mw.util.getParamValue('user');

      if (user) {

      pageName += '/' + Morebits.string.toUpperCaseFirstChar(user);

      }

      }

      if (pageName) {

      var li = mw.util.addPortletLink('p-cactions', '#', 'W-Ping', 'ca-wping', 'Schedule a watchlist ping for this page');

      li.addEventListener('click', function(e) {

      e.preventDefault();

      WPing.pingDialog(pageName);

      });

      }

      }

      }).catch(function(err) {

      console.error('[W-Ping]:', err);

      });

      //