User:SD0001/W-Ping.js

From Wikipedia, the free encyclopedia
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.
/* jshint maxerr: 999 */

// <nowiki>
$.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({
	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');

				delete opt[page];
				WPing.updatePingList(opt).then(function() {
					status.info('Done');
					mw.track('counter.gadget_WPing.ping_cancelled');
					window.setTimeout(function() {
						Window.close(); // close dialog
					}, 300);
				}).catch(function(err) {
					status.error('Failed to cancel: ' + 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');

	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(function(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 = $('<span>');
	durations.forEach(function(e) {
		$('<a>').addClass('wping-prompt').text(e).appendTo($quickSelect);
	});
	datepicker.$element.after($quickSelect);

	$quickSelect.find('a').css({
		'padding': '0 5px 0 5px'
	}).click(function(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');

	var opt = JSON.parse(mw.user.options.get('userjs-wping-list'));
	if (!opt) {  // for the first-time user
		opt = {};
	}
	opt[page] = [ pingtime, reason ];

	WPing.updatePingList(opt).then(function() {
		status.info('Done');
		mw.track('counter.gadget_WPing.ping_saved');

		// automatically close window in a short while
		window.setTimeout(function() {
			$(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');
            }
		}
	}).catch(function(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 = $('<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, function(_, target, text) {
				if (!target) {
					target = text;
				}
				return '<a href="' + mw.util.getUrl(target) + '" title="' + target + '">' + text + '</a>';
			});
			
			var histlink = mw.Title.newFromText(page).namespace < 0 ? 'hist' : 
				('<a href="' + mw.util.getUrl(page, { action: 'history' }) + '">hist</a>');

			$('<li>').addClass('wping-line').attr('data-page', page).html(
				'(' + histlink + ') ' +
				'<span class="mw-changeslist-separator"></span> ' +
				'<a href="' + mw.util.getUrl(page) + '" title="' + page + '">' + page + '</a> ' +
				'<span class="mw-changeslist-separator"></span> ' +
				(reason ? '<i>(' + reason + ')</i> <span class="mw-changeslist-separator"></span> ' : '') +
				'[ <a href=# class="wping-snooze">snooze</a> | <a href=# class="wping-dismiss">dismiss</a> ]'
			).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(
		$('<div>').attr('id', 'wping').append(
			$('<h4>').text('Pings'),
			$ul
		)
	);

	// check if pinged pages exists, if not turn the links red, occurs lazily
	// XXX: only works if there are <50 pages
	WPing.api.get({ titles: pingPages }).then(function(json) {
		$.each(Object.values(json.query.pages), function(pageid, data) {
			if (data.missing === '') {
				$ul.find('a[href="' + mw.util.getUrl(data.title) + '"]').addClass('new');
			}
		});
	});

	$ul.find('.wping-snooze').click(function(e) {
		e.preventDefault();
		var page = $(e.target).parent().data('page');
		WPing.pingDialog(page);
	});

	$ul.find('.wping-dismiss').click(function(e) {
		e.preventDefault();
		var page = $(e.target).parent().data('page');

		delete opt[page];
		WPing.updatePingList(opt);
		WPing.removePingDisplayLine(page);
		mw.track('counter.gadget_WPing.ping_dismissed');
	});

};

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 = $('<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, function(_, target, text) {
			if (!target) {
				target = text;
			}
			return '<a href="' + mw.util.getUrl(target) + '" title="' + target + '">' + text + '</a>';
		});
		$ul.append(
			$('<li>').html(
				'<a href="' + mw.util.getUrl(page) + '" title="' + page + '">' + page + '</a>: ' +
				(reason ? '(' + reason + ') ' : '') +
				moment(time).utcOffset(timezone).format('HH:mm, D MMMM YYYY')
			)
		);
	});

	$('#mw-content-text').append(
		$('<p>').text('A ping shall be delivered to your watchlist for the following pages, at the specified time in ' + WPing.getTimeZoneString(timezone) + ' time zone:'),
		$ul
	);

	WPing.api.get({ titles: Object.keys(opt) }).then(function(json) {
		$.each(Object.values(json.query.pages), function(pageid, data) {
			if (data.missing === '') {
				$ul.find('a[href="' + mw.util.getUrl(data.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);
});

// </nowiki>