User:SD0001/T-Watch.js

/** * Script for enabling temporary watchlisting of pages * */

/* jshint maxerr: 999 */

//

var api;

$.when(	mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api', 'mediawiki.Title', 'moment']), 	$.ready ).then(function {

api = new mw.Api;

// Menu interface if (mw.config.get('wgNamespaceNumber') >= 0) {

// For vector skin, make a submenu within the "more" dropdown, inspired by MoreMenu var hasMenu = false; if (mw.config.get('skin') === 'vector') { hasMenu = true; $(mw.util.addPortletLink('p-cactions', '#', 'T-Watch...', 'ca-twatch')).css({				'position': 'relative'			}).append(				$('').addClass('menu').css({ 'display': 'none', 'background-color': '#fff', 'border': '1px solid #aaa' })			).click(function(e) {				e.preventDefault;			}).on('mouseenter', function {				$(this).find('.menu').css({ 'left': $(this).outerWidth, 'top': '-1px', 'position': 'absolute' }).show;			}).on('mouseleave', function {				$(this).find('.menu').hide;			});

} else if (mw.config.get('skin') === 'monobook') { hasMenu = true; $(mw.util.addPortletLink('p-cactions', '#', 'T-Watch...', 'ca-twatch')).css({				'position': 'relative',				'padding-bottom': '0'			}).append(				$('').addClass('menu').css({ 'display': 'none', 'z-index': '1000', 'list-style': 'none', 'background-color': '#fff', 'border': '1px solid #aaa', 'margin': '0' })			).click(function(e) {				e.preventDefault;			}).on('mouseenter', function {				$(this).find('.menu').css({ 'left': '0px', 'top': $(this).outerHeight, 'position': 'absolute' }).show;			}).on('mouseleave', function {				$(this).find('.menu').hide;			}); mw.util.addCSS(				'#ca-twatch .menu li { display: list-item; border: none; }' +				'#ca-twatch .menu li a { background: none !important; }' + // !important needed for IE/firefox				'#ca-twatch .menu li:hover { text-decoration: underline; }'			); }

var menuItems = $.isArray(window.TWatch_Durations_viewing) ? window.TWatch_Durations_viewing : ['1 week', '1 month'];

menuItems.forEach(function(duration) {			var li = mw.util.addPortletLink(hasMenu ? 'ca-twatch' : 'p-cactions', '#', 'Watch – ' + duration, '', 'Watchlist this page for a duration of ' + duration);			li.addEventListener('click', function(ev) { ev.preventDefault; var watchTill = moment.add(parseDuration(duration)); watchPage(mw.config.get('wgPageName'), watchTill.unix * 1000); });		});	}

// Edit page interface if (mw.config.get('wgAction') === 'edit' || mw.config.get('wgAction') === 'submit') {

var $select = $(' ').attr('id', 'watchduration').css({			'margin-left': '5px'		}).change(function {			$('#wpWatchthis')[0].checked = true;		}).insertAfter($('#wpWatchthisWidget').parent.next);

var options = $.isArray(window.TWatch_Durations_editing) ? window.TWatch_Durations_editing : ['1 week', '2 weeks', '1 month', '2 months'];

options.forEach(function(durtext) {			var watchTill = moment.add(parseDuration(durtext));			$(' ')				.text(durtext)				.val(watchTill.unix * 1000)				.appendTo($select);		}); $(' ').text('Indefinitely').val('inf').prop('selected', true).appendTo($select);

if (window.TWatch_default_edit_watch_period) { $select.find('option:contains("' + window.TWatch_default_edit_watch_period + '")').prop('selected', true); }

// record in pages object that the page is to be unwatched for said duration // watching of the page is done by mediawiki $('#wpSave').click(function {			if ($('#wpWatchthis')[0].checked) {				var dur = $select.val;				if (dur === 'inf') {					return;				}				recordAsWatching(mw.config.get('wgPageName'), parseInt(dur));			}		}); }

// Integration with user scripts that edit pages (probably unnecessary) hookEventListener;

// Special page to see list of temporarily watched pages if (mw.config.get('wgPageName') === 'Special:BlankPage/TempWatched' ||		mw.config.get('wgPageName') === 'Special:BlankPage/T-Watch' ||		mw.config.get('wgPageName') === 'Special:TempWatched') { buildSpecialPage; }

// show 'expiring soon' alerts if (mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' && !window.TWatch_NoAlerts) { mw.hook('wikipage.content').add(addWatchlistAlerts); }

// Unwatch expired pages every hour var nextCheckTime = mw.user.options.get('userjs-twl-nextcheck'); if (!nextCheckTime) { // for first-time users api.saveOption('userjs-twl-nextcheck', (new Date.getTime + 1000*60*60).toString); }	else if (new Date.getTime > parseInt(nextCheckTime)) { removeExpiredPages; }

}).catch(console.error);

/** * @param {string} page * @param {number} till_time - milliseconds since epoch */ function watchPage(page, till_time) { api.watch(page).done(function {		recordAsWatching(page, till_time);		var title = new mw.Title(page);		mw.notify( '"' + title.toText + '" and its ' + (title.isTalkPage ? 'associated subject' : 'talk') + ' page have been added to your watchlist till ' + getString(till_time, true) );	}).fail(function(err) {		mw.notify('Couldn\'t add to watchlist due to an error. Please try again.\n Error: ' + JSON.stringify(err));	}); }

/** * @param {string} page * @param {number} till_time - milliseconds since epoch */ function recordAsWatching(page, till_time) { page = new mw.Title(page).getSubjectPage.toText; // normalize talk page to subject page var opt = JSON.parse(mw.user.options.get('userjs-twl-pages')); if (!opt) { opt = {}; }	opt[page] = till_time; // expiry timestamp api.saveOption('userjs-twl-pages', JSON.stringify(opt)); }

function removeExpiredPages { var opt = JSON.parse(mw.user.options.get('userjs-twl-pages')); if (!opt) return; var pagesToUnwatch = []; $.each(opt, function(page, expiry) {		if (new Date.getTime > expiry) {			pagesToUnwatch.push(page);		}	});

// kludge: if more than 50 pages, unwatch 50 for now, and leave the rest for the next hour if (pagesToUnwatch.length > 50) { pagesToUnwatch = pagesToUnwatch.slice(0, 50); }	api.unwatch(pagesToUnwatch).done(function {		// check again for expired pages after an hour		api.saveOption('userjs-twl-nextcheck', (new Date.getTime + 1000*60*60).toString);

// update pages object pagesToUnwatch.forEach(function(page) {			delete opt[page];		}); api.saveOption('userjs-twl-pages', JSON.stringify(opt)); }); }

function addWatchlistAlerts { var opt = JSON.parse(mw.user.options.get('userjs-twl-pages')); if (!opt) { return; }	var threshold = moment.add(parseDuration(window.TWatch_Alert_period || '3 days')); $('.mw-changeslist-title').each(function {		var page = this.textContent;		if (opt[page] && moment(opt[page]).isBefore(threshold)) {			var $container = $(this).parent.parent.parent; // li element			if ($container.find('.twatch-expiry-alert').length === 0) {				var exptext = moment(opt[page]).fromNow;				$(' ')					.text('[expires ' + exptext + ']')					.attr('title', 'This page will be removed from your watchlist around ' + getString(opt[page], true))					.addClass('twatch-expiry-alert')					.css({ 'font-size': '80%', 'padding-left': '5px', 'color': 'brown' }).appendTo($container);			}		}	}); }

/** * @param {String} str - a string specifying duration - eg. "3 weeks", "2 months" * @returns {moment.duration} - moment Duration object */ function parseDuration(str) { var i;	for (i = 0; i < str.length; i++) { if (str[i] < '0' || str[i] > '9') { break; }	}	var num = parseInt(str); var text = str.slice(i).trim; var momentObject = moment.duration(num, text); if (!momentObject._isValid || momentObject.asMilliseconds === 0) { console.error('Invalid duration string: "' + str + '"'); }	return momentObject; }

function hookEventListener { mw.hook('record_watch').add(function(arg) {		if (!arg) arg = {};		arg.page = arg.page || mw.config.get('wgPageName');		arg.setting = arg.setting || 'preferences'; // allows input like window.ScriptNameWatchPref		arg.duration = arg.duration || window.tempWatchlistDefaultDuration || 'inf';		arg.action; // 'edit', 'create', 'upload', 'move', 'delete', 'rollback'

if (arg.setting === 'watch' || arg.setting === true) { if (arg.duration !== 'inf') recordAsWatching(arg.page, arg.duration); api.watch(arg.page); // client script should do this ideally, but just in case... } else if (arg.setting === 'preferences') { // consult user's site preferences var pref; switch (arg.action) { case 'create': pref = 'watchcreations'; break; case 'move': pref = 'watchmoves'; break; case 'delete': pref = 'watchdeletions'; break; case 'upload': pref = 'watchuploads'; break; case 'rollback': pref = 'watchrollbacks'; break; default: pref = 'watchdefault'; }			if (mw.user.options.get(pref) == 1) { // dunno whether its string or number if (arg.duration !== 'inf') recordAsWatching(arg.page, arg.duration); api.watch(arg.page); }		}	}); }

function buildSpecialPage { $('#firstHeading').text('Temporarily watched pages'); document.title = 'Temporarily watched pages'; $('#mw-content-text').empty;

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

var $ul = $(''); $.each(opt, function(page, expiry) {		$ul.append( $('').html(				''+ page + ': ' +				getString(expiry)			) );	});	$('#mw-content-text').append(		$(' ').text('The following pages are set to be automatically unwatched after the given time in ' + getTimeZoneString + ' time zone:'),		$(' ').html('This list may include any pages that you may have subsequently unwatched manually, click here to purge such pages.'),		$ul	); $('#purgeunwatchedpages').click(function {

$ul.replaceWith('Purging...'); var arrayOfPages = Object.keys(opt); // ASSUME < 50 for now if (arrayOfPages.length > 50) { alert('You have more than 50 pages here: purge feature coming soon'); return; }		api.get({			"action": "query",			"format": "json",			"prop": "info",			"titles": arrayOfPages,			"inprop": "watched"		}).then(function(json) {			Object.values(json.query.pages).forEach(function(info) { if (info.watched === undefined) { delete opt[info.title]; }			});			opt = JSON.stringify(opt);			api.saveOption('userjs-twl-pages', opt).then(function { mw.user.options.set('userjs-twl-pages', opt); buildSpecialPage; });		});

// var arrayOfArrays = arrayChunk(arrayOfPages, 50); // arrayOfArrays.forEach(function(array) {		// 	api.get({ // 		"action": "query", // 		"format": "json", // 		"prop": "info", // 		"titles": array, // 		"inprop": "watched" // 	}).then(function(json) { // 		Object.values(json.query.pages).forEach(function(info) {		// 			if (info.watched === undefined) {		// 				delete opt[info.title.replace(/ /g, '_')];		// 			}		// 		}); // 	});		// });

}); }

// HELPER FUNCTIONS:

/** * @param {number} date - milliseconds since epoch */ function getString(date, withzone) { return moment(date).utcOffset(getUserTimeZone).format('HH:mm, D MMMM YYYY') + (withzone ? (' (' + getTimeZoneString + ').') : ''); }

function getUserTimeZone { if (window.userTimeZone) { // cache it		return window.userTimeZone; }	var pref = mw.user.options.get('timecorrection'); if (pref.indexOf('ZoneInfo|') === 0) { window.userTimeZone = parseInt(pref.slice('ZoneInfo|'.length)); } else if (pref.indexOf('Offset|') === 0) { window.userTimeZone = parseInt(pref.slice('Offset|'.length)); } else if (pref === 'System|0') { window.userTimeZone = 0; } else { console.error('[W-Ping]: unparsable time zone: ' + pref); }	return window.userTimeZone; }

function getTimeZoneString(timecorrection) { timecorrection = timecorrection || getUserTimeZone; 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; }

// function arrayChunk(arr, size) { // 	var result = []; // 	var current; // 	for (var i = 0; i < arr.length; ++i) { // 		if (i % size === 0) { // when 'i' is 0, this is always true, so we start by creating one. // 			current = []; // 			result.push(current); // 		} // 		current.push(arr[i]); // 	} // 	return result; // }

//