MediaWiki:Gadget-WatchlistNotice.core.js

From wikishia

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.
/***********************************************************************
 ************************ WatchlistNotice ******************************
 ***********************************************************************
 ***********************************************************************
 *  @description
 *  This is part of the Commons Notice system, formerly known as
 *  Watchlist Notice. The system provides support for targeting messages
 *  to users who share a common property like living in the same country
 *  or being administrators and allows them to dismiss messages.
 *
 *  It also empowers the user to choose which messages should be displayed
 *  in which manner.
 *
 *  More information at [[Help:Watchlist messages]].
 *
 *
 *  Porting the gadget to other Wikis:
 *  You need everything listed at [[Help:Watchlist messages]] +
 *  all ext.gadget-dependencies. You can see all the dependencies at
 *  [[MediaWiki:Gadgets-definition]] and [[Special:Gadgets]]
 *
 *  You have to register all the dependencies at [[MediaWiki:Gadgets-definition]].
 *  You have to register the loader-gadget and the core at [[MediaWiki:Gadgets-definition]].
 *
 *
 *  @author
 *  Original Authors: [[:en:w:User:Ruud Koot|Ruud Koot]], [[commons:User:Dschwen]]
 *  New jQuery version by: [[User:Whym]]
 *  A lot of addidional featurs by: [[User:Rillke]]
 *
 *  @jsvalidate
 *  Please make sure keeping this script jshint valid
 *
 *  @terminology
 *  The term notice or note covers the whole section or mechanism
 *  One unit containing information is referenced as "message"
 *
 *  @documentation
 *  [[Help:Watchlist messages]]
 *
 *  <nowiki>
 */

/*global jQuery:false, mediaWiki:false, Geo:false*/
/*jshint curly:false, undef:true, unused:true */

(function(mw, $) {
	'use strict';
	var $notice = $('#watchlist-message');
	if ($notice.length === 0) return;

	// These messages will be overwritten by localized versions
	// The loacalized versions are shiped with the watchlist
	// They are inlcuded in a hidden div and make use of [[Help:Autotranslate]]
	var i18n = {
		'dwn-dlg-title': "{{SITENAME}} notice configuration",
		'dwn-dlg-save': "Save",
		'dwn-dlg-cancel': "Cancel",
		'dwn-dlg-saving': "Saving your preferences",
		'dwn-dlg-save-success': "{{SITENAME}} notice configuration saved and will be applied after page was reloaded",
		'dwn-dlg-save-err': "Options could not be saved",

		'dwn-type-h': "Message topics and types",
		'dwn-type-desc': "Select the topics you would like to get notifications about",
		'dwn-type-summary': "You will receive messages about $1",

		'dwn-display-h': "Display",
		'dwn-display-desc': "Adjust how the messages are displayed",
		'dwn-display-fade': "Show only one message at once",
		'dwn-display-collapse': "Fold notice section if it is empty",
		'dwn-display-disable': "Disable {{SITENAME}} notices",
		'dwn-display-disabled-info': "{{SITENAME}} notices disabled. The gadget can be reactivated in your preferences.",
		'dwn-display-not-enabled': "The {{SITENAME}} notice gadget is already disabled. The effect will be visible after reloading this page.",
		'dwn-display-cycle': "Step through the messages automatically",

		'dwn-colon': ":",
		'dwn-cfg': "Config notices",
		'dwn-go-msg': "Go to message $1",

		'dwn-mark-as-read': "read",
		'dwn-mark-as-read-details': "Hide this message",

		'dwn-no-msg': "There are no new {{SITENAME}} messages."
	};
	mw.messages.set(i18n);

	// The CSS that will be used for blocking the dialog
	// when options were changed and we are waiting for an AJAX request
	var blockCSS = {
		border: 'none',
		padding: '15px',
		backgroundColor: '#000',
		borderRadius: '10px',
		opacity: 0.5
	};

	// Small helper functions returning the message text for a given key
	// The _msgp parses the message (so e.g. wikilinks are resolved)
	var _msg = function(params) {
			/*jshint unused:false */
			var args = Array.prototype.slice.call(arguments);
			args[0] = 'dwn-' + args[0];
			return mw.msg.apply(mw, args);
		},
		_msgp = function(params) {
			/*jshint unused:false */
			var args = Array.prototype.slice.call(arguments);
			args[0] = 'dwn-' + args[0];
			var msg = mw.message.apply(mw, args);
			return msg.parse();
		};




	// dwn stands for Dismiss Watchlist Note
	var dwn = {
		// Always bump this number when making changes
		version: '1.0.9.0',
		pefKey: 'dwn',
		dismissKey: 'dwnd',
		$notice: $notice,
		$noticeInner: $('#watchlist-message-inner'),
		$types: $('#wln-types'),
		$msgs: $('.watchlist-message'),
		storageTTL: 14, // in days (1 day = 24 hours)
		defaultconfig: {
			types: {},      // Types included here will be _un_subscribed-by-default
			fade: true,     // Whether to use a fading effect or whether to show all messages at once
			collapse: true, // Whether to close the watchlist message section if it is empty
			cycle: true,    // Whether to step through the messages automatically
			duration: 10    // (currently no UI option for this): Duration to show a message.
					// The duration is automatically adapted to the text's length.
		},
		/**
		* Loads the required modules and calls the method to show the configuration dialog
		* This function may be added added as an jquery observer for a click-event
		*
		* @example
		*      $('#myButton').click(dwn.configScreen);
		*
		* @param e {Object} A jQuery Event object.
		* @context {Object} May be a jQuery objcet but is not required
		* @return {undefined}
		*/
		configScreen: function(e) {
			if (e && $.isFunction(e.preventDefault)) e.preventDefault();
			if (dwn.$prefLink.hasClass('ui-state-disabled')) return;
			dwn.$prefLink.addClass('ui-state-disabled');

			var toLoad = ['jquery.ui', 'ext.gadget.libJQuery', 'ext.gadget.jquery.blockUI'];

			mw.loader.using(toLoad, function() {
				mw.libs.settingsManager.fetchGadgetSetting(dwn.pefKey, ['option']).done(function(prefName, settingValue) {
					dwn.config = $.extend(true, dwn.defaultconfig, settingValue);
					dwn._configScreen();
				});
			});
		},
		/**
		* Shows the configuration dialog as depicted in
		* [[File:2013-05-23-Gadget-WatchlistNotice-Config-Dlg.png]]
		*
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't assume anything. It's a UI method.
		*/
		_configScreen: function() {
			var $dlg,
				$dlgContent1, $typeWrap1, $typeInfoAjax, $typeInfoList,
				$dlgContent2, $typeWrap2, $typeInfoSummary, $displayPrefWrap,
				$displayPrefFadeW, $displayPrefFade, $displayPrefCollapseW, $displayPrefCollapse,
				$displayPrefCycleW, $displayPrefCycle, $displayDisable,
				$clonedTypes,
				$types = $(),
				selected = [],
				dlgButtons = {};

			$dlg = $('<div>').css('padding', 0);
			$('<div id="wln-version"></div>').text(dwn.version).appendTo($dlg);
			$dlgContent1 = $('<div class="wln-dlg-content"></div>').appendTo($dlg);
			$('<h3>').text(_msg('type-h')).appendTo($dlgContent1);
			$typeWrap1 = $('<div>').appendTo($dlgContent1);
			$('<p>').text(_msg('type-desc')).appendTo($typeWrap1);
			$typeInfoAjax = $('<div class="wln-prefscreen-dynamic"></div>').css('max-height', Math.max(Math.min($(window).height() - 500, 400), 150)).appendTo($dlg);
			$typeInfoList = $('<ol>').appendTo($typeInfoAjax);
			$dlgContent2 = $('<div class="wln-dlg-content"></div>').appendTo($dlg);
			$typeWrap2 = $('<div>').appendTo($dlgContent2);
			$typeInfoSummary = $('<p class="wln-dlg-summary"></p>').appendTo($typeWrap2);
			$('<h3>').text(_msg('display-h')).appendTo($dlgContent2);
			$displayPrefWrap = $('<div>').appendTo($dlgContent2);
			$('<p>').text(_msg('display-desc')).appendTo($displayPrefWrap);
			$displayPrefFadeW = $('<div>').appendTo($displayPrefWrap);
			$displayPrefFade = $('<input id="wln_pref_disp_fade" type="checkbox"/>').appendTo($displayPrefFadeW);
			$('<label for="wln_pref_disp_fade"></label>').text(_msg('display-fade')).appendTo($displayPrefFadeW);
			$displayPrefCollapseW = $('<div>').appendTo($displayPrefWrap);
			$displayPrefCollapse = $('<input id="wln_pref_disp_collapse" type="checkbox"/>').appendTo($displayPrefCollapseW);
			$('<label for="wln_pref_disp_collapse"></label>').text(_msg('display-collapse')).appendTo($displayPrefCollapseW);
			$displayPrefCycleW = $('<div>').appendTo($displayPrefWrap);
			$displayPrefCycle = $('<input id="wln_pref_disp_cycle" type="checkbox"/>').appendTo($displayPrefCycleW);
			$('<label for="wln_pref_disp_cycle"></label>').text(_msg('display-cycle')).appendTo($displayPrefCycleW);
			$displayDisable = $('<button type="button" role="button"></button>').text(_msgp('display-disable')).button().appendTo($displayPrefWrap);


			/*
				Save settings
			*/
			dlgButtons[_msg('dlg-save')] = function() {
				dwn.config.types = {};
				$clonedTypes.not('.ui-selected').each(function(i, el) {
					dwn.config.types[$(el).data('id')] = 1;
				});
				dwn.config.fade = $displayPrefFade[0].checked;
				dwn.config.collapse = $displayPrefCollapse[0].checked;
				dwn.config.cycle = $displayPrefCycle[0].checked;

				$dlg.closest('.ui-dialog').block({
					css: blockCSS,
					message: '<h3 style="color:#fff">' + mw.html.escape(_msg('dlg-saving')) + '</h3>'
				});

				mw.libs.settingsManager.switchGadgetPref(dwn.pefKey, dwn.config).done(function() {
					$dlg.closest('.ui-dialog').unblock().fadeOut(function() {
						mw.notify($('<div class="wln-ok-sign"></div>').text(_msgp('dlg-save-success')));
						$dlg.dialog('close');
					});
				}).fail(function() {
					mw.notify($('<div class="wln-err-sign"></div>').text(_msg('dlg-save-err')));
					$dlg.closest('.ui-dialog').unblock();
				});

			};
			dlgButtons[_msg('dlg-cancel')] = function() {
				$dlg.dialog('close');
			};

			$displayDisable.click(function() {
				var g = mw.libs.settingsManager.gadget( 'WatchlistNotice' );
				if (g.isEnabled()) {
					g.disable(function() {
						$dlg.closest('.ui-dialog').unblock().fadeOut(function() {
							// Security hint: You must parse this message when setting HTML!
							mw.notify($('<div class="wln-ok-sign"></div>').html(_msgp('display-disabled-info')));
							$dlg.dialog('close');
						});
					}, function() {
						mw.notify($('<div class="wln-err-sign"></div>').text(_msg('dlg-save-err')));
						$dlg.closest('.ui-dialog').unblock();
					});
					$dlg.closest('.ui-dialog').block({
						css: blockCSS,
						message: '<h3 style="color:#fff">' + mw.html.escape(_msg('dlg-saving')) + '</h3>'
					});
				} else {
					mw.notify($('<div class="wln-ok-sign"></div>').text(_msgp('display-not-enabled')));
				}
			});

			dwn.$types.find('tr').each(function(i, el) {
				if (!i) return; // Skip first row, which is the header row
				var $tr = $(el),
					$tds = $tr.find('td'),
					id = $.trim($tds.eq(0).text()),
					ts = $.trim($tds.eq(1).text()),
					c = dwn.config,
					$type = $('<li>').data({
						id: id,
						ts: ts
					});

				if (!c.types[id]) {
					$type.addClass('ui-selected');
				}

				$('<b>').text(ts + _msg('colon')).appendTo($type);
				$('<div>').text($tds.eq(2).text()).appendTo($type);
				$types = $types.add($type);
			});

			/*
				Apply settings
			*/
			$clonedTypes = $types.clone(true).addClass('ui-selectee').click(function() {
				selected = [];
				$(this).toggleClass('ui-selected');
				$clonedTypes.filter('.ui-selected').each(function(i, el) {
					selected.push($(el).data('ts'));
				});
				$typeInfoSummary.text(_msg('type-summary', selected.join(', ')));
			}).mousedown(function(e) {
				if (1 === e.which) $(this).addClass('ui-selecting');
			}).mouseup(function() {
				$(this).removeClass('ui-selecting');
			}).mouseleave(function() {
				$(this).removeClass('ui-selecting');
			}).appendTo($typeInfoList);

			$displayPrefFade[0].checked = dwn.config.fade;
			$displayPrefCollapse[0].checked = dwn.config.collapse;
			$displayPrefCycle[0].checked = dwn.config.cycle;


			/*
				Show a dialog
			*/
			$dlg.dialog({
				position: {
					my: 'right top',
					at: 'right bottom',
					of: dwn.$noticeInner
				},
				title: _msgp('dlg-title'),
				dialogClass: 'wln-prefscreen',
				resizable: false,
				buttons: dlgButtons,
				close: function() {
					$dlg.remove();
					dwn.$prefLink.removeClass('ui-state-disabled');
				},
				open: function() {
					var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
					$buttons.eq(0).specialButton('proceed');
					$buttons.eq(1).specialButton('cancel');
				}
			});
			$dlg.closest('.ui-dialog').find('.ui-dialog-titlebar-close')._jqInteraction();
		},
		/**
		* Creates a link that belongs to one message
		*
		* @example
		*      var $link = dwn._$msgLink(1, $myMessage);
		*
		* @param i {number} The ordinal number of the message.
		* @param $el {object} Instance of jQuery: The message the link will belong to.
		* @context {any} May be called in and from all contexts.
		* @return {object} Instance of jQuery: The created link.
		*/
		_$msgLink: function(i, $el) {
			var $b = $('<a>').attr({
				href: '#wln' + i,
				title: _msg('go-msg', (i + 1)),
				'class': 'ui-state-default'
			}).css({
				display: 'inline-block',
				padding: '1px',
				'text-align': 'center',
				width: '1.4em'
			}).text(i + 1).click(dwn._goToMessage);
			$el.data('$b', $b);
			$b.data('$msg', $el);

			return $b._jqInteraction();
		},
		/**
		* Sets a timeout which will, when expired, show the next message
		*
		* @example
		*      dwn._rotationTimeout(factor);
		*
		* @param factor {number} Numbers smaller than 1 (but > 0) will speed-up
		*           cycling through all messages, numbers bigger than 1 will slow it down.
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		_rotationTimeout: function(t) {
			if (dwn.rotationtimeout) clearTimeout(dwn.rotationtimeout);
			var textlen = dwn.$lastShownNote ? dwn.$lastShownNote.text().length : 150;
			dwn.rotationtimeout = setTimeout(function() {
				dwn.rotation();
			}, ((t || 1) * 1000 * dwn.config.duration * (textlen/150) + 1000));
		},
		/**
		* Loads the required modules and calls the method,
		* which will show the navigation panel
		*
		* @example
		*      dwn.panel();
		*
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		panel: function() {
			mw.loader.using('ext.gadget.libJQuery', dwn._panel);
		},
		/**
		* Shows a navigation panel consisting of numbers and a link
		* that opens the configuration
		*
		* @context {Object} May be called in and from all contexts.
		* @return {any} Don't rely on it. It's a UI method.
		*/
		_panel: function() {
			if (dwn.$panel) dwn.$panel.remove();

			var $panel = dwn.$panel = $('<div class="wln-panel-wrap"></div>'),
				$messagePanel = $('<div class="wln-msg-panel"></div>').appendTo($panel),
				$prefLink = dwn.$prefLink = $('<a>').text(_msg('cfg')).attr({
					href: '#wln_prefs',
					'class': 'ui-state-default wln-cfg-link'
				}).appendTo($panel);

			$.createIcon('ui-icon-gear').prependTo($prefLink);

			// There is no $.Widget.prototype._focusable() yet
			dwn.$prefLink._jqInteraction().click(dwn.configScreen);

			dwn.$notice.css('position', 'relative').append($panel).hover(function() {
				$panel.stop(true).fadeTo('fast', 1);
				dwn._rotationTimeout(2);
			}, function() {
				$panel.stop(true).delay(500).fadeTo(1000, 0);
				dwn._rotationTimeout(0.5);
			});

			dwn.$msgs.each(function(i, el) {
				dwn._$msgLink(i, $(el)).appendTo($messagePanel);
			});
			if (dwn.config.cycle) dwn.startRotation();
		},
		/**
		* A callback passed to a jQuery observer which is bound to the
		* numbers on the navigation panel. Extracts the ordinal number of
		* the message to got to from the href attribute
		* and calls goToMessage with that number
		*
		* @example
		*      $('#myNumber').click(dwn._goToMessage);
		*
		* @param e {Object} A jQuery Event object.
		* @context {Object} Instance of jQuery.
		*      The link must contain the ordinal number of the message in the following format: #wln0000
		*      where 0000 is a number of any digit length.
		* @return {undefined}
		*/
		_goToMessage: function(e) {
			if (e && $.isFunction(e.preventDefault)) e.preventDefault();
			var m = $(this).attr('href').match(/\#wln(\d+)/);
			dwn.goToMessage(Number(m[1]), 50);
		},
		/**
		* Shows a specific message, identified by an ordinal number at the notice section
		*
		* @example
		*      dwn.goToMessage(1);
		*
		* @param i {number} Message number (starting with 0)
		* @param duration {number} Transition duration in ms
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		goToMessage: function(i, duration) {
			dwn.lastShownNote = i;

			var _onready2Show = function() {
				dwn.$lastShownNote = dwn.$msgs.eq(i);
				dwn.$lastShownNote.fadeIn(duration, function() {
					dwn.$lastShownNote.data('$b').addClass('ui-state-highlight');
				});
				if (!dwn.config.fade) {
					dwn.$noticeInner.clearQueue().animate({ scrollTop: dwn.$lastShownNote.position().top }, duration);
				}
				dwn._rotationTimeout();
			};
			if (dwn.$lastShownNote && dwn.$lastShownNote.filter(':visible').length) {
				dwn.$lastShownNote.data('$b').removeClass('ui-state-highlight');
				if (dwn.config.fade) {
					dwn.$lastShownNote.fadeOut(duration, _onready2Show);
				} else {
					_onready2Show();
				}
			} else {
				_onready2Show();
			}
		},
		/**
		* Kicks-on cycling through all messages
		*
		* @example
		*      dwn.startRotation();
		*
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		startRotation: function() {
			// No need to rotate over just one message
			if (dwn.$msgs.length <= 1) {
				dwn.$msgs.eq(0).show();
				return;
			}
			// Hide all notices
			if (dwn.config.fade) dwn.$msgs.hide();
			// A randam message
			dwn.goToMessage(Math.round(Math.random() * (dwn.$msgs.length - 1)));
		},
		/**
		* Wrangles with numbers to find the next message in the list of messages
		* After it found the correct message to show, it calls goToMessage with its findings
		*
		* @example
		*      dwn.rotation();
		*
		* @param duration {number} Transition duration in ms
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		rotation: function() {
			if (dwn.$msgs.length <= 1 || !dwn.config.cycle) return;
			dwn.lastShownNote++;
			if (dwn.lastShownNote >= dwn.$msgs.length) dwn.lastShownNote = 0;
			dwn.goToMessage(dwn.lastShownNote);
		},
		/**
		* Removes a messages from the list of those
		* (Happens when user clicks hide/mark as read)
		*
		* @example
		*      dwn.removeMessage();
		*
		* @param $msg {object} instance of jQuery containing a message node
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		removeMessage: function($msg) {
			dwn.$msgs = dwn.$msgs.not($msg.remove());
			dwn.checkIfEmpty();
			dwn.panel();
		},
		/**
		* Bunch of functions that determine whether a message
		* is addressed to the current user
		*
		* @example
		*      dwn.isTarget.geo(data);
		*
		* @param {Object} d A data object. Data is read from the HTML-DOM. The object given will
		*  be modified by this script (e.g. `d.countries` converted from string to array).
		* @param {string} [d.countries]
		* @return {boolean} true, if the message is possibly for the user, false if it is for sure not
		*/
		isTarget: {
			geo: function(d) {
				var found = false;

				// The object is always set (if the server could not derive geo data from the
				// IP address, it is set to an empty object). But this covers the case where
				// the request might have failed so the variable would be undefined.
				// Below each property access to Geo must take into account that the value may
				// be undefined. Comparison like `x === Geo.city` can be done straight up,
				// but `Geo.country.toUpperCase` will throw an exception since undefined has
				// no method #toUpperCase (only string has).
				if (!window.Geo) {
					return true;
				}

				if (d.countries) {
					d.countries = d.countries.split(':');
					if (Geo.country) {
						$.each(d.countries, function(i, c) {
							c = $.trim(c.toUpperCase());
							if (c === Geo.country.toUpperCase()) {
								found = true;
								return false;
							}
						});
						if (!found) return false;
					}
				}

				if (d.cities) {
					found = false;
					d.cities = d.cities.split(':');
					$.each(d.cities, function(i, c) {
						c = $.trim(c);
						if (c === Geo.city) {
							found = true;
							return false;
						}
					});
					if (!found) return false;
				}

				if (d.lonFrom && d.lonTo) {
					var lon = parseFloat(Geo.lon, 10),
						lonFrom = parseFloat(d.lonFrom, 10),
						lonTo = parseFloat(d.lonTo, 10),
						lonDiff = lonTo - lonFrom;

					if (lonDiff < 0) {
						// We crossed the International Date Line
						if (lonTo > lon || lonFrom > lon) return false;
					} else {
						if (lonTo < lon || lonFrom > lon) return false;
					}
				}
				if (d.latFrom && d.latTo) {
					var lat = parseFloat(Geo.lat, 10),
						latFrom = parseFloat(d.latFrom, 10),
						latTo = parseFloat(d.latTo, 10);

					if (latFrom < lat || latTo > lat) return false;
				}

				return true;
			},
			pref: function(d) {
				var opt = d.preferences,
					cfg = dwn.config,
					t = d.type,
					found = false;

				if (opt) {
					opt = opt.split(';');
					$.each(opt, function(i, optval) {
						var k, v,
							m = optval.split('!=');
						if (m && m.length === 2) {
							k = m[0]; v = m[1];

							if ($.trim(v) !== $.trim((mw.user.options.get(k) + ''))) {
								found = true;
								// break the .each loop
								return false;
							}
						} else if ((m = optval.split('=')) && m.length === 2) {
							k = m[0]; v = m[1];

							if ($.trim(v) === $.trim((mw.user.options.get(k) + ''))) {
								found = true;
								// break the .each loop
								return false;
							}
						}
					});
				} else {
					found = true;
				}

				if (t && cfg && cfg.types) {
					found = found && !cfg.types[t];
				}
				return found;
			},
			browser: function(d) {
				var clnt = $.client.profile(),
					clntName = clnt.name.toLowerCase(),
					bfound = false,
					ok = true;

				if (d.browser) {
					var bs = d.browser.split(':');
					$.each(bs, function(i, b) {
						if ($.trim(b.toLowerCase()) === clntName) {
							bfound = true;
							return false;
						}
					});
					ok = ok && bfound;
				}

				if (d.browserVerMin) ok = ok && (parseFloat(clnt.version, 10) >= parseFloat(d.browserVerMin, 10));
				if (d.browserVerMax) ok = ok && (parseFloat(clnt.version, 10) <= parseFloat(d.browserVerMax, 10));
				if (d.browserLang) {
					var langs = ['', d.browserLang.toLowerCase(), ''].join(':'),
						clntLang = dwn.isTarget.getBrowserLanguage().toLowerCase(),
						clntLangFull = ['', clntLang, ''].join(':'),
						clntLangBase = ['', clntLangBase.split('-')[0], ''].join(':');

					ok = ok && (langs.indexOf(clntLangFull) >= 0 || langs.indexOf(clntLangBase) >= 0);
				}
				return ok;
			},
			getBrowserLanguage: function() {
				return navigator.userLanguage || navigator.language || navigator.browserLanguage;
			},
			other: function(d) {
				// Dummy function; will be called during verification
				// Required because not all data is used for determining whether the message
				// should be shown
				var m = d.until.match(/\d+/g);
				if (m) {
					m[1]--;
					d.until = new Date(Date.UTC.apply(Date, m));
					d.until.setDate(d.until.getDate()+2);
				} else {
					d.until = null;
				}
				return true;
			}
		},
		/**
		* Collection of data that will be read by the following method
		*
		* @example
		*      var data = dwn.readData();
		*
		* @param $msg {object} instance of jQuery containing a message node
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		data: {
			geo: ['countries', 'cities', 'latFrom', 'latTo', 'lonFrom', 'lonTo'],
			pref: ['preferences', 'type'],
			browser: ['browser', 'browserVerMin', 'browserVerMax', 'browserLang'],
			other: ['until']
		},
		readData: function($msg) {
			var ret = {};
			$.each(dwn.data, function(k, arr) {
				var dm = ret[k] = {};
				$.each(arr, function(i, dataKey) {
					dm[dataKey] = $.trim($msg.find('.wln_' + dataKey).text());
				});
			});
			return ret;
		},
		// Removes expired keys
		tidyDismissKeys: function() {
			var d = new Date();
			$.each(dwn.dkv, function(k, v) {
				if (!v || new Date(v) < d) {
					delete dwn.dkv[k];
				}
			});
		},
		/**
		* Whether one message was hidden will be stored here
		* Old browsers do not support DOM storage, we'll use bad
		* old cookies in this case. More about cookies below
		* in function install.
		*
		* Also, an async process saves this setting to the user options
		*
		* @example
		*      var info = dwn.store.load('myKey');
		*
		* @param key {string} a key for which
		*      a lookup in the store will be performed
		* @context {any} May be called in and from all contexts.
		* @return {any} The value found in the storage key.
		*/
		store: {
			load: function(key) {
				if (!this.backend) this.init();
				var ret;
				if (dwn.dkv[key]) return 1;
				if ('cookie' === this.backend) {
					ret = $.cookie(key);
				} else {
					var stored = this.readWithTTL()[key];
					ret = stored ? stored.val : undefined;
				}
				if (ret) this.makePersistent(key, ret, dwn.storageTTL);
				return ret;
			},
			readWithTTL: function() {
				var stored = mw.storage.getObject(this.storageKey);
				if (!stored) {
					return {};
				}
				var now = Date.now();
				var out = {};
				for (var key in stored) {
					if (stored[key].expires > now) {
						out[key] = stored[key];
					}
				}
				mw.storage.setObject(this.storageKey, out);
				return out;
			},
			save: function(key, val, TTL) {
				if (!this.backend) this.init();

				if ('cookie' === this.backend) {
					$.cookie(key, val, {
						expires: TTL || dwn.storageTTL, // expires in 7 days
						path: '/' // entire commonswiki
					});
				} else {
					var stored = this.readWithTTL();
					stored[key] = {val: val, expires: Date.now() + ((TTL || dwn.storageTTL) * 86400000)};  // 1000*60*60*24
					mw.storage.setObject(this.storageKey, stored);
				}

				this.makePersistent(key, val, TTL);
			},
			makePersistent: function(key, val, TTL) {
				// WARNING: Val(ue) is ignored!
				var d = new Date();
				dwn.tidyDismissKeys();
				d.setDate(d.getDate() + TTL);
				dwn.dkv[key] = d;
				if (this.to) clearTimeout(this.to);
				this.to = setTimeout(function() {
					mw.libs.settingsManager.switchGadgetPref(dwn.dismissKey, dwn.dkv);
				}, 5000);
			},
			init: function() {
				this.backend = "localStorage";
				this.storageKey = "mwgadget-watchlistnotice";
			}
		},
		/**
		* Reads the translation from the HTML-DOM and overwrites the English default
		* translation
		*
		* @example
		*      dwn.readTranslation();
		*
		* @context {any} May be called in and from all contexts.
		* @return {undefined}
		*/
		readTranslation: function() {
			var $i18n = $('#wln-translation'),
				i18nNew = {};

			$.each(i18n, function(k) {
				var t = $i18n.find('.' + k).text();
				if (t) {
					i18nNew[k] = t;
				}
			});
			mw.messages.set(i18nNew);
		},
		/**
		* Removes hidden messages from the HTML-DOM and updates
		* the message list
		*
		* @example
		*      dwn.updateMessageList();
		*
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		updateMessageList: function() {
			// We want only messages that are not hidden
			dwn.$msgs.not(':visible').remove();
			dwn.$msgs = dwn.$msgs.filter(':visible');
		},
		/**
		* Checks whether the message list is empty.
		* If so adds a message that there are no messages
		* or - dependent on the user's preferences - folds
		* the notice section
		*
		* @example
		*      dwn.checkIfEmpty();
		*
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		checkIfEmpty: function() {
			if (0 === dwn.$msgs.length) {
				if (dwn.config.collapse && '#noticenohide' !== location.hash) {
					return dwn.$notice.slideUp('fast', function() {
						dwn.$notice.remove();
					});
				}
				$('<li class="wln-no-message"></li>').text(_msg('no-msg')).appendTo(dwn.$notice.find('ul'));
			}
		},
		/**
		* First checks how many messages are addressed to the user
		* and how the user prefers to have them displayed and then
		* calls the appropriate methods to build the UI
		* This is the only (partially) remaining part of the old script
		*
		* @example
		*      dwn.install();
		*
		* @context {any} May be called in and from all contexts.
		* @return {any} Don't rely on it.
		*/
		install: function() {
			var originalHeight = dwn.$noticeInner.height(),
				maxMessageHeight,
				d = new Date();

			dwn.$msgs.each(function(i, el) {
				var $msg = $(el),
					// mId = message id
					mId = Number($msg.attr('class').replace(/.*cookie\-ID\_(\d*).*/ig, '$1')),
					// hwm stands for "hide watchlist message" but Cookies are always sent to the server with *each* request
					// and often upload bandwidth is very limited.
					cKey = 'hwm-' + mId,
					dismissed = dwn.store.load(cKey),
					// ".*?" is the junk we want to remove
					cleanCls = $msg.attr('class').replace(/.*?(watchlist\-message.+)$/, '$1'),
					vis = $msg.filter(':visible'),
					data = dwn.readData($msg),
					TTL;

				// Cleaning class attribute
				$msg.attr('class', cleanCls);

				// Check whether the message has been previously dismissed
				if (0 === vis.length || dismissed) {
					return $msg.hide();
				}

				var showMsg = true;
				$.each(data, function(k, data) {
					showMsg = showMsg && dwn.isTarget[k](data);
				});
				if (!showMsg) return $msg.hide();

				// Ensure that the message is visible after cleaning up the class attribute
				// We put it here for performace reasons
				$msg.show();

				//                                             1000 * 60 * 60 * 24
				TTL = data.other.until ? ((data.other.until - d) / (86400000)) : dwn.storageTTL;

				var $ButtonLink = $('<a>').attr({
					href: '#hide',
					title: _msg('mark-as-read-details', 1)
				}).text(_msg('mark-as-read')).click(function(e) {
					e.preventDefault();
					dwn.store.save(cKey, 1, TTL);
					dwn.removeMessage($msg);
				});
				var $markRead = $('<span class="wln-mark-as-read"></span>').append($('<span>').text('[')).append($ButtonLink).append($('<span>').text(']'));
				$msg.append(' ', $markRead);
				// Check whether a single message is bigger than the container
				maxMessageHeight = $msg.height() + 7;
			});

			if (dwn.config.fade) {
				// Only animate, if one message requires more space
				if (maxMessageHeight > originalHeight) {
					dwn.$noticeInner.animate({
						height: maxMessageHeight
					});
				}
			} else {
				// per [[Special:Permalink/96962449]] (Help talk:Watchlist messages)
				// let it extend so it fits the content
				dwn.$noticeInner.css('min-height', dwn.$noticeInner.height());
				dwn.$noticeInner.css('height', 'auto');
			}

			dwn.updateMessageList();
			dwn.checkIfEmpty();
			dwn.panel();
			// Look for a hashlink, and if it is present, show the config screen - this is documented!
			if ('#noticenohide' === location.hash) dwn.configScreen();
		}
	};

	// Expose globally
	mw.libs.dismissWatchlistNote = dwn;

	mw.loader.using(['mediawiki.util', 'mediawiki.user', 'user.options', 'jquery.cookie', 'mediawiki.storage', 'ext.gadget.SettingsManager'], function() {
		var fetch = mw.libs.settingsManager.fetchGadgetSetting;
		fetch(dwn.pefKey, ['option']).done(function(prefName, settingValue) {
			fetch(dwn.dismissKey, ['option']).done(function(dkn, dkv) {
				dwn.config = $.extend(true, dwn.defaultconfig, settingValue);
				dwn.dkv = dkv || {}; // Dismiss-key-value
				dwn.readTranslation();
				dwn.install();
			});
		});
	});

})(mediaWiki, jQuery);

// </nowiki>