User:Ruslik0/Gadget-Favorites.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
 /**
 * User interface enhancement to star images and add them to a personal favorites gallery
 * @author [[User:Dschwen]], 2013
 */
/* jshint laxcomma: true, smarttabs: true */
/* global mw,$ */

mw.loader.using('mediawiki.util').then(function () {
$(function(){
	// only run on file pages
	if (mw.config.get('wgNamespaceNumber')!=6 || !window.localStorage) return;
	
	var user = mw.user.getName()
	  , title = new mw.Title(mw.config.get('wgPageName'))
	  , file = title.getMain()
	  , favesPage = 'User:' + user + '/Favorites'
	  , ls = window.localStorage
	  , favCache = JSON.parse(ls.getItem('favCache')||'{}')
	  , isFaved = !!favCache[file]
	  , link = mw.util.addPortletLink( $('#p-views').length ? 'p-views' : 'p-cactions', '#', '-', 'ca-fave', '-',undefined, '#ca-view')
	  , hasPersonalLink = false
	  , editToken = mw.user.tokens.get('csrfToken');

	// toggle link
	function toggleLink() {
		var a = $('a',link);
		a.text(isFaved ? 'Unfave' : 'Fave');
		a.prop('title', isFaved ? 'Remove from favorites' : 'Add to favorites');
	}
	toggleLink();

	// add a link in the top right row next to teh watchlist link (this might be too much clutter)
	function addPersonalLink() {
		// do favCache.keys().length ? what about support
		for (var k in favCache) { 
			if (favCache.hasOwnProperty(k)) {
				if (!hasPersonalLink) {
					mw.util.addPortletLink( 'p-personal', '/wiki/'+favesPage, 'Favorites', 'pt-fave', 'Your favorite images', undefined, '#pt-watchlist');
					hasPersonalLink = true;
				}
				break;
			}
		}
	}

	// refresh fave status (on window focus)
	function refreshFaveStatus() {
		favCache = JSON.parse(ls.getItem('favCache')||'{}');
		isFaved = !!favCache[file];
		toggleLink();
	}
	$(window).on('focus',refreshFaveStatus);

	// load /Favorites page
	function loadFavorites(callback) {
		// normalize the gallery tag placement in the receved text
		function normalizeGalleryTags(text) {
			// check if gallery tags are on the page
			if (!/<[Gg]allery[\s>]/.test(text)) {
				// no: prepend them on top
				text = '<gallery>\n</gallery>\n' + text;
			} else {
				// make sure nothing comes before a closing gallery tag on the same line
				text = text.replace(/([^\n])<\/([Gg])allery>/,'$1\n</$2allery>');
				// make sure nothing comes after an opening gallery tag on the same line
				text = text.replace(/<([Gg])allery([^>]*)>([^\n])/,'<$1allery$2>\n$3');
			}
			callback(text);
		}

		// fetch raw text
		$.get(mw.util.wikiScript('index'), { action: 'raw', title: favesPage }, undefined, 'text')
		.done(normalizeGalleryTags)
		.fail(function(xhr,a,b) {
			if (xhr.status===404) {
				// The /Favorites page does not yet exist, initialize empty page
				normalizeGalleryTags('');
			} else if (xhr.status===200 && xhr.responseText) {
				// sometimes jquery throws a parse error (even though we requested the dataType to be 'string'!)
				mw.notify("Come on Fabrice, this should not happen! ("+xhr.status+","+a+","+b+")");
				normalizeGalleryTags(xhr.responseText);
			} else {
				mw.notify("Unable to load Favorites. ("+xhr.status+","+a+","+b+")");
				ls.removeItem('favLock');
			}
		});
	}

	// save picks function
	function saveFavorites(text, callback) {
		// call API
		$.post( mw.util.wikiScript('api'), {
			format: 'json',
			action: 'edit',
			title: favesPage,
			summary: 'Saving Favorites with [[MediaWiki:Gadget-Favorites.js]]',
			text: text,
			token: editToken
		})
		.done(callback)
		.fail(function() { mw.notify("Unable to save Favorites."); })
		.always(function() {
			// remove the lock in either case
			ls.removeItem('favLock');
		});
	}

	function commitTransactions() {
		var lock = parseInt(ls.getItem('favLock')||"0",10)
		  , now = new Date(), time = now.getTime();

		// check if lock is set and if so, was it set less than a minute ago?
		if (lock>0 && (time-lock)<(60*1000)) {
			// already running, try again in 5 seconds
			setTimeout(commitTransactions,5000);
		}
		ls.setItem('favLock',time);

		// load the /Favorites page
		loadFavorites(function(text){
			// fetch transactions again (in case page loading took a long time)
			var trans = JSON.parse(ls.getItem('favTrans')||'{}')
			  , applied = 0;

			// to be executed when all transactions are applied
			function transactionsApplied() {
				// fetch transactions again (in case page saving took a long time)
				var newTrans = JSON.parse(ls.getItem('favTrans')||'{}'), file;
				
				// now remove all transactions in newTrans that are identical to the transactions in trans that we just processed
				for (file in trans) {
					if (trans.hasOwnProperty(file) && file in newTrans && trans[file].action==newTrans[file].action) {
						delete newTrans[file];
					}
				}
				ls.setItem('favTrans',JSON.stringify(newTrans));
			}

			// process the page text and apply transactions
			var line = text.split('\n'), n=line.length, i
			  , newLine = [], token, file, title, norm
			  , galleryFound = false, inGallery=false;
			for (i=0; i<n; ++i) {
				if (inGallery) {
					if (/<\/[Gg]allery>/.test(line[i])) {
						// closing the current gallery block
						inGallery = false;
					} else {
						// parsing an image line in a gallery block
						token = line[i].split('|');
						title = new mw.Title(token[0]);
						norm = title.getMain();
						// remove any image that is in the transaction list (both add and rem!)
						if (norm in trans) {
							// remove image from /Favorites page (by not adding it to newLine[])
							applied++;
							continue;
						}
					}
				} else {
					if (/<[Gg]allery[\s>]/.test(line[i])) {
						// opening of a new gallery block
						inGallery = true;
						if (!galleryFound) {
							// this is the first gallery block, add new faves on top
							newLine.push(line[i]);
							for (file in trans) {
								if (trans.hasOwnProperty(file) && trans[file].action=="add") {
									newLine.push("File:"+file+'|"' + file + '" by [[User:'+trans[file].author+']]');
									applied++;
								}
							}
							galleryFound = true;
							continue;
						}
					}
				}
				newLine.push(line[i]);
			}

			// were any changes applied to the /Favorites page?
			var newText = newLine.join('\n');
			if (applied>0) {
				// yes, save the new /Favorites page text
				saveFavorites(newText, transactionsApplied);
			} else {
				// no, consider the transactions processed
				transactionsApplied();
			}

			// we now know the supposed contents of the /Favorites page, might as well use it to make sure the favCache is up to date
			refreshFaveCache(newText);
		});
	}

	// process the page text of the /Favorites gallery page and update the favCache
	function refreshFaveCache(text) {
		// process the page text and rebuild favorites Cache
		favCache={};
		var line = text.split('\n'), n=line.length, i
		  , now = new Date(), time = now.getTime()
		  , inGallery=false, token, title, norm;
		for (i=0; i<n; ++i) {
			if (inGallery) {
				if (/<\/[Gg]allery>/.test(line[i])) {
					// closing the current gallery block
					inGallery = false;
				} else {
					// parsing an image line in a gallery block
					token = line[i].split('|');
					title = new mw.Title(token[0]);
					norm = title.getMain();
					favCache[norm]=1;
				}
			} else {
				if (/<[Gg]allery[\s>]/.test(line[i])) {
					// opening of a new gallery block
					inGallery = true;
				}
			}
		}
		// store cache in localStorage
		ls.setItem('favCache', JSON.stringify(favCache));
		// set timestamp for last refresh
		ls.setItem('favTimestamp', time);
		refreshFaveStatus();
		addPersonalLink();
	}

	// thank uploader using the Thanks API (thanks is not journaled, if the tab is closed too early.. ...well, shucks)
	function thankUploader() {
		// callback to deploy the actual thanks request after we found out the 1st revision id
		function sendThanks(data) {
			var firstRev = data.query.pages[data.query.pageids[0]].revisions[0].revid;
			// thanks API request
			$.get( mw.util.wikiScript('api'), {
				action: 'thank',
				rev: firstRev,
				source: 'Favorites Gadget',
				token: editToken
			});
		}

		// first get the id of the first revision of the current file page
		$.get( mw.util.wikiScript('api'), {
				format: 'json',
				action: 'query',
				titles: mw.config.get('wgPageName'),
				indexpageids: true,
				prop: 'revisions',
				rvdir: 'newer',
				rvlimit: 1
			})
		.done(sendThanks)
		.fail( function() { mw.notify("Unable to thank Uploader."); } );
	}
	
	// hook portlet link handler
	$(link).on('click',function(e){
		// change faved flag
		isFaved = !isFaved;
		
		if (isFaved) {
			// now insert into favCache if it is a favorite
			favCache[file]=1;
		} else {
			// or delete from cache if unfaved
			delete favCache[file];
		}

		// store in localStorage
		ls.setItem('favCache', JSON.stringify(favCache));

		// determine image author/uploader
		var author = $('.filehistory a.mw-userlink').first().clone().find('.adminMark').remove().end().eq(0).text();

		// add transaction (use localStorage to share data across tabs, if the servers are really slow and multiple images were faved before the edit to /Favorites is made)
		var trans = JSON.parse(ls.getItem('favTrans')||'{}');
		trans[file] = { action: isFaved?"add":"rem", author:author };
		ls.setItem('favTrans', JSON.stringify(trans));
		commitTransactions();
		if (isFaved) { thankUploader(); }
		
		// change link appearance and description to reflect new operation
		toggleLink();
		
		e.preventDefault();
	});

	// check if we have pending transactions from an aborted save
	function checkPendingTasks() {
		var trans = JSON.parse(ls.getItem('favTrans')||'{}'), pending=0, t;
		for (t in trans) if (trans.hasOwnProperty(t)) pending++;
		if (pending>0) {
			commitTransactions();
		} else {
			// check if we need to refresh the favorites cache (every 15mins)
			var cacheTime = parseInt(ls.getItem('favTimestamp')||"0",10)
			  , now = new Date(), time = now.getTime();
			if (cacheTime===0 || (time-cacheTime)>(15*60*1000)) {
				loadFavorites(refreshFaveCache);
			} else {
				addPersonalLink();
			}
		}
	}	
	checkPendingTasks();
});
});