User:Pathoschild/Scripts/globalview.js

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/********
** GlobalView
** This experimental script adds a [[Special:GlobalView]] page on the current
** wiki to view your new messages and unread notifications on all wikis.
** 
** This is a quick and dirty script — it's not ready for reuse! Please contact
** me if you're interested in using it.
********/
/* global $, mw */
var pathoschild = pathoschild || {};

/**
 * Creates an HTML5 progress bar with an interface to update the progress.
 * @param {int} value The current progress value.
 * @param {int} total The maximum progress value.
 */
pathoschild.ProgressBar = function(value, max) {
	/*********
	** Fields
	*********/
	var self = {
		/**
		 * The HTML5 progress bar.
		 */
		progressBar: $('<progress></progress>', { value: value, max: max }),

		/**
		 * The current progress value.
		 */
		value: value,

		/**
		 * The maximum progress value.
		 */
		max: max,

		/**
		 * A promise which is completed when the progress bar hits 100%.
		 */
		promise: $.Deferred(),

		/**
		 * Tracks the result of watched queries for troubleshooting.
		 */
		watched: {
			'all': [],
			'done': [],
			'failed': []
		}
	};


	/*********
	** Public methods
	*********/
	/**
	 * Append the HTML5 progress bar to an element.
	 * @param {jQuery|string} A jQuery element or selector to which to append the progress bar.
	 * @returns The progress bar instance for chaining.
	 */
	self.appendTo = function(parent) {
		self.progressBar.appendTo(parent);
		return self;
	};

	/**
	 * Increment the progress value.
	 * @returns The progress bar instance for chaining.
	 */
	self.increment = function() {
		self.value += 1;
		self.progressBar.attr('value', self.value);
		if(self.value == self.max)
			self.promise.resolve();
		return self;
	};

	/**
	 * Watch a promise and increment when it completes.
	 * @param {string} key A unique key which identifies the watched promise for troubleshooting.
	 * @param {jQuery.Deferred} promise A promise to watch.
	 * @returns The progress bar instance for chaining.
	 */
	self.watch = function(key, promise) {
		self.watched.all.push(key);
		promise
			.then(function() { self.watched.done.push(key); self.increment(); })
			.fail(function() { self.watched.failed.push(key); self.increment(); });
	};

	return self;
};


/**
 * Implements the [[Special:GlobalView]] page.
 */
pathoschild.GlobalView = function() {
	var self = {};

	/*********
	** Public methods
	*********/
	/**
	 * Render the [[Special:GlobalView]] UI.
	 */
	self.initialiseUI = function() {
		// get context
		if(mw.config.get('wgCanonicalNamespace') != 'Special' || mw.config.get('wgTitle') != 'GlobalView')
			return; // only initialise on [[Special:GlobalView]]
		var username = mw.config.get('wgUserName');

		// bootstrap UI
		mw.loader.using(['ext.echo.styles.badge', 'ext.echo.styles.notifications']); // load badge styles
		$('#firstHeading, title:first').text('Global view');
		var page = $('#mw-content-text').empty().append($('<p></p>', { text: 'Hi ' + username + '!' }));
		var placeholder = $('<div></div>').appendTo(page);

		// fetch wikis
		placeholder.text('↻ fetching unified wikis...');
		self.getUnifiedWikis().then(function(wikis) {
			// add UI
			page.append($('<h3></h3>', { text: 'Messages & notifications' }));
			placeholder.text('↻ scanning wikis...');
			var progress = new pathoschild.ProgressBar(0, wikis.length).appendTo(placeholder);

			// fetch messages & notifications
			var hasNotifications = false;
			$.each(wikis, function(w, wiki) {
				progress.watch(wiki.wiki,
					self.getUserData(wiki.url).then(function(data) {
						// collect notifications
						var list = [];
						if('messages' in data.userinfo)
							list.push($('<div class="new-message"><a href="' + wiki.url + '/wiki/User talk:' + username + '">You have new messages.</a></div>'));
						if(data.notifications.length) {
							$.each(data.notifications, function(n, notification) {
								var html = $(notification['*']);

								// fix local links
								html.find('a').attr('href', function(i, href) {
									return href.match(/^\/w/)
										? wiki.url + href
										: href;
								});
								list.push(html);
							});
						}

						// build UI
						if(list.length) {
							hasNotifications = true;
							page.append([
								$('<dt></dt>').append(
									$('<a></a>', { href: wiki.url, text: wiki.wiki })
								),
								$('<dd></dd>').append(
									$('<ul></ul>').append(list)
								)
							]);
						}
					})
				);
			});

			// update UI when done
			progress.promise.then(function() {
				if(!hasNotifications)
					page.append('You have no notifications or messages. :(');
				placeholder.remove();
			});
		});
	};

	/**
	 * Get a list of wikis attached to the current user's global account.
	 * The schema is { wiki: 'aawiki', url: 'https://aa.wikipedia.org' }.
	 * @returns A promise which provides an array of wikis on completion.
	 */
	self.getUnifiedWikis = function() {
		return self.getApi().then(function(api) {
			return api.get({ action: 'query', meta: 'globaluserinfo', guiprop: 'merged' }).then(function(data) {
				return $.map(data.query.globaluserinfo.merged, function(wiki) {
					// exclude non-wikis
					if(['loginwiki', 'ukwikimedia'].indexOf(wiki.wiki) === -1)
						return wiki;
				});
			});
		});
	};

	/**
	 * Get user info and notifications from a remote wiki.
	 * @param {string} url The wiki's root URL (like 'https://aa.wikipedia.org').
	 * @returns A promise which provides user info.
	 */
	self.getUserData = function(url) {
		return self.getApi(url).then(function(api) {
			return api
				.get({
					action: 'query',
					meta: ['userinfo', 'notifications'],
					notprop: 'list',
					notformat: 'html',
					uiprop: 'hasmsg'
				})
				.then(function(data) {
					return {
						userinfo: data.query.userinfo,
						notifications: $.map(data.query.notifications.list, function(notification) {
							if(!('read' in notification))
								return notification;
						})
					};
				});
		});
	};

	/**
	 * Get an API client for a given wiki.
	 * @param {string} url The wiki's root URL (like 'https://aa.wikipedia.org'), or nothing for the current wiki.
	 * @returns A promise which provides an API wrapper.
	 */
	self.getApi = function(url) {
		return mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(function() {
			return !url || url.indexOf(mw.config.get('wgServer')) !== -1
				? new mw.Api()
				: new mw.ForeignApi(url + '/w/api.php');
		});
	};

	return self;
};

// initialise
new pathoschild.GlobalView().initialiseUI();