User:Colin M/scripts/MarkAsRead.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* TODO
- make rows actually disappear when read (optionally?)
	- should 'mark all' find all rows with that title and remove them too? oof.
- would be cool if the 'mark all' button could be hidden if there are no newer revisions for this page. 
	- theoretically, could probably do this without extra requests, just by looking at the DOM
*/
function lastRevForLine(line) {
	let rev = line.dataset.mwRevid;
	if (rev) return rev;
	for (let row of line.querySelectorAll('tr.mw-changeslist-line')) {
		if (row.dataset.mwRevid) {
			return row.dataset.mwRevid;
		}
	}
}

const include_mark_all = true;
function addButtons(line, api) {
	const revid = lastRevForLine(line);
	//if (revid === undefined) return;
	//const ts = parseInt(line.dataset.mwTs);
	//const ts = line.dataset.mwTs;
	// Hm, so setting timestamp to exactly ts seems to mark everything up to but not including that rev as read. Hack required?
	// timestamps look like 20190827165313 i.e. 2019-08-27-16-53-13
	// there must be some mw helper for encoding/decoding date strings like this...
	const title_ele = line.querySelector('a.mw-changeslist-title');
	if (!title_ele) {
		// This shouldn't happen. Was previously triggered when we weren't excluding entries from move log or page curation log.
		console.warn("Couldn't find a.mw-changeslist-title for line:", line);
		return;
	}
	const href = title_ele.attributes.href.value;
	const prefix = '/wiki/';
	let title;
	if (href.startsWith(prefix)) {
		title = href.slice(prefix.length);
	} else {
		const pre2 = '/w/index.php?title=';
		if (href.startsWith(pre2)) {
			title = href.slice(pre2.length);
		} else {
			console.warn("Couldn't find title in...", title_ele);
			return;
		}
	}
	const row = line.querySelector('tr');
	// Main watchlist entry ele (with links to article, diff, history, etc.). This is where we add the button(s).
	const button_td = row.querySelector('td.mw-changeslist-line-inner');
	const addButton = (label, onclick) => {
		const button = document.createElement('button');
		button.textContent = label;
		button.addEventListener('click', onclick);
		button_td.appendChild(button);
		button.classList.add('markread');
		button.style.padding = '0 2px';
		button.style.marginLeft = '0.3em';
		button.style.marginRight = '1.3em';
		button.style.borderColor = '#555';
		button.style.borderWidth = '2px';
		return button;
	};
	// TODO: Guess don't need to import mw.Title anymore?
	//const parsed_title = mw.Title.newFromText(title);
	const markChangeRead = (evt) => {
		const params = {
			action: 'setnotificationtimestamp',
			// Wait, then what the heck does the "revids" param do?
			//torevid: parseInt(revid)+1,
			newerthanrevid: revid,
			// need only one of titles or revids
			// But if we provide torevid/newerthanrevid WITHOUT title, the API call will fail silently. Because of course it will.
			// need to get rid of any % encoding
			titles: decodeURI(title)
		};
		console.log(params);
		api.postWithEditToken(params);
		line.style.opacity = 0.5;
	};
	const markAllRead = (evt) => {
		// If no timestamp/revid is provided, default behaviour is to set timestamp to now (i.e. mark all extant revisions as read)
		const params = {
			action: 'setnotificationtimestamp',
			titles: decodeURI(title)
		};
		console.log(params);
		api.postWithEditToken(params);
		line.style.opacity = 0.3;
	};
	const heavycheck = '✔';
	const check = '✓';
	addButton(check, markChangeRead);
	if (include_mark_all) {
		const markall = addButton(heavycheck, markAllRead);
		markall.style.backgroundColor = '#d4b3ad';
		markall.setAttribute('title', 'Mark all revisions of this page read');
	}
}

if (mw.config.values.wgCanonicalSpecialPageName === 'Watchlist') {
	mw.loader.using(['mediawiki.api', 'mediawiki.Title'], function () {
		let api = new mw.Api();
		window.wgAPI = api;
		let wl = document.querySelector('.mw-changeslist');
		// Exclude log lines (move log, page curation log, etc.)
		let lines = wl.querySelectorAll('table.mw-changeslist-line:not(.mw-changeslist-log)');
		lines.forEach((line) => addButtons(line, api));
	});
}
/* re API...
So, the docs at https://www.mediawiki.org/wiki/API:Setnotificationtimestamp aren't totally clear at this, but
based on some experimentation, it seems like for each (page, user) pair, there's just one revid/timestamp stored, such
that anything newer is considered unseen, and anything older is seen.
This is totally contrary to my mental model, which was that there was a seen flag for every revision. I didn't realize that viewing the diff of one
particular rev of a page would cause all older revs to be marked as viewed. 
Well this affects the implementation of this - and actually makes it a lot easier.
*/
/* Notes on watchlist DOM structure:
.mw-changeslist
	<h4>date
	div
		table.mw-changeslist-line data-mw-revid=... data-mw-ts=...
			tr
		// or, for collapsed line with multiple revs (will have data-ts but not revid) data-mw-revid will be on the tr's
		table.mw-chageslist-line.mw-collapsible > tbody
			tr*
*/