Jump to content

User:Daniel Quinlan/Scripts/Catatonic.js

From Wikipedia, the free encyclopedia
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.
'use strict';

mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter', 'user.options']).then(async () => {
	if (mw.config.get('wgNamespaceNumber') !== 14) return;

	const DEFAULT_OPTIONS = { pages: [] };
	const NOTIFY_OPTIONS = { autoHide: false, title: 'Catatonic' };
	const pageId = mw.config.get('wgRelevantArticleId');
	const api = new mw.Api();
	let options = await getOptions();

	if (mw.config.get('wgAction') === 'info') {
		addToggle();
	} else if (options.pages.includes(pageId)) {
		addCSS();
		addTags();
	}

	async function addToggle() {
		function updateLink() {
			link.textContent = `${listed ? 'disable' : 'enable'} activity tags`;
			link.title = `Catatonic ${listed ? 'enabled' : 'disabled'}`;
		}
		const td = document.querySelector('#mw-pageinfo-article-id td:nth-child(2)');
		if (!td) return;
		let listed = options.pages.includes(pageId);
		const link = document.createElement('a');
		link.href = '#';
		updateLink();
		link.addEventListener('click', async event => {
			event.preventDefault();
			try {
				listed = !listed;
				if (listed) options.pages.push(pageId);
				else options.pages.splice(options.pages.indexOf(pageId), 1);
				options.pages = [...new Set(options.pages)].sort((a, b) => a - b);
				await saveOptions(options);
				options = await getOptions();
				updateLink();
				showListedPages();
			} catch (error) {
				mw.notify(`Error: ${error}`, { ...NOTIFY_OPTIONS, type: 'error' });
				link.textContent = 'error';
				link.style.pointerEvents = 'none';
			}
		});
		td.append(' (', link, ')');
	}

	function addCSS() {
		mw.util.addCSS(`
			.catatonic-inactivity { color: var(--color-base, #808080); }
			.catatonic-inactivity[data-weeks] { color: var(--color-error, #de5a49); }
			.catatonic-inactivity[data-weeks="1"] { color: var(--color-warning, #a97e2a); }
			.catatonic-inactivity[data-weeks="0"] { color: var(--color-success, #229679); }
		`);
	}

	async function addTags() {
		const formatTimeAndDate = mw.loader.require('mediawiki.DateFormatter').formatTimeAndDate;
		const articlePath = mw.config.get('wgArticlePath');
		const articleStatic = articlePath.replace('$1', '');
		const articleRegex = new RegExp(articlePath.replace('$1', '([^#?]+)'));
		const nodes = document.querySelectorAll(`#mw-content-text .mw-category li > a[href*="${articleStatic}"]`);
		const pages = new Map();
		for (const link of nodes) {
			const li = link.closest('li');
			if (!li) continue;
			const match = link.getAttribute('href')?.match(articleRegex);
			const title = match ? decodeURIComponent(match[1]).replace(/_/g, ' ') : link.textContent;
			if (title) pages.set(title, li);
		}
		for (const group of batch(Array.from(pages.keys()), 50)) {
			const titles = group.join('|');
			try {
				const result = await api.get({
					action: 'query',
					format: 'json',
					formatversion: 2,
					prop: 'revisions',
					rvprop: 'timestamp',
					titles
				});
				for (const page of result?.query?.pages || []) {
					const li = pages.get(page.title);
					if (!li) continue;
					const span = document.createElement('span');
					span.className = 'catatonic-inactivity';
					const ts = page.revisions?.[0]?.timestamp;
					if (ts) {
						const days = Math.floor(daysSince(ts));
						span.dataset.days = days;
						span.dataset.weeks = Math.floor(days / 7);
						span.dataset.months = Math.floor(days / 30);
						span.textContent = days;
						span.title = `last activity: ${formatTimeAndDate(new Date(ts))}`;
					} else {
						span.textContent = 'unknown';
						span.title = 'last activity: unknown';
					}
					li.append(' \u00b7 ', span);
				}
			} catch (error) {
				console.error('Catatonic: addTags error', error);
			}
		}
	}

	async function getOptions() {
		try {
			const stored = mw.user.options.get('userjs-catatonic') || '{}';
			const parsed = JSON.parse(stored);
			return { ...DEFAULT_OPTIONS, ...parsed };
		} catch (error) {
			console.error('Catatonic: getOptions error', error);
			await saveOptions(DEFAULT_OPTIONS);
			return DEFAULT_OPTIONS;
		}
	}

	async function saveOptions(options) {
		try {
			const value = JSON.stringify(options);
			if (mw.user.isNamed()) await api.saveOption('userjs-catatonic', value);
			mw.user.options.set('userjs-catatonic', value);
		} catch (error) {
			console.error('Catatonic: saveOptions error', error);
			throw error;
		}
	}

	function showListedPages() {
		const message = document.createElement('div');
		message.textContent = 'Listed pages: ';
		if (options.pages.length) {
			options.pages.forEach((id, i) => {
				if (i > 0) message.append(', ');
				const a = document.createElement('a');
				a.href = mw.util.getUrl(null, { curid: id, action: 'info' });
				a.textContent = id;
				a.title = `Page information for ${id}`;
				a.target = '_blank';
				message.append(a);
			});
		} else {
			message.append('none');
		}
		mw.notify(message, { ...NOTIFY_OPTIONS, tag: 'catatonic-list' });
	}

	function batch(items, max) {
		const bins = Math.ceil(items.length / max);
		const size = Math.ceil(items.length / bins);
		const batches = [];
		for (let i = 0; i < items.length; i += size) {
			batches.push(items.slice(i, i + size));
		}
		return batches;
	}

	function daysSince(timestamp) {
		return (new Date() - new Date(timestamp)) / (1000 * 60 * 60 * 24);
	}
});