/**
* Script for enabling temporary watchlisting of pages
*
*/
/* jshint maxerr: 999 */
// <nowiki>
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 [[meta: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(
$('<ul>').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(
$('<ul>').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 = $('<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));
$('<option>')
.text(durtext)
.val(watchTill.unix() * 1000)
.appendTo($select);
});
$('<option>').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();
$('<span>')
.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 = $('<ul>');
$.each(opt, function(page, expiry) {
$ul.append(
$('<li>').html(
'<a href="' + mw.util.getUrl(page) + '" title="' + page + '">'+ page + '</a>: ' +
getString(expiry)
)
);
});
$('#mw-content-text').append(
$('<p>').text('The following pages are set to be automatically unwatched after the given time in ' + getTimeZoneString() + ' time zone:'),
$('<p>').html('This list may include any pages that you may have subsequently unwatched manually, <a id="purgeunwatchedpages">click here to purge such pages</a>.'),
$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;
// }
// </nowiki>