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.
// <nowiki>
// @ts-check
// Companion to markblocked - asynchronously marks locked users
// Chunks borrowed from [[m:User:Krinkle/Scripts/CVNSimpleOverlay_wiki.js]],
// [[User:GeneralNotability/ip-ext-info.js]], and [[MediaWiki:Gadget-markblocked.js]]
(()=> {
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php', { userAgent: 'Marked-locked-userscript' });
const localApi = new mw.Api({ userAgent: 'Marked-locked-userscript' });
/**
 * Get all userlinks on the page
 *
 * @param {JQuery} $content page contents
 * @return {Map} list of unique users on the page and their corresponding links
 */
function lockedUsers_getUsers($content) {
	const userLinks = new Map();

	// Get all aliases for user: & user_talk: (taken from markblocked)
	const userNS = [];
	for (const ns in mw.config.get( 'wgNamespaceIds' ) ) {
		if (mw.config.get('wgNamespaceIds')[ns] === 2 || mw.config.get('wgNamespaceIds')[ns] === 3) {
			userNS.push(mw.util.escapeRegExp(ns.replace(/_/g, ' ')) + ':');
		}
	}

	// RegExp for all titles that are  User:| User_talk: | Special:Contributions/ (for userscripts)
	const userTitleRX = new RegExp('^(' + userNS.join('|') + '|Special:Contrib(?:ution)?s\\/|Special:CentralAuth\\/)+([^\\/#]+)$', 'i');
	const articleRX = new RegExp(mw.config.get('wgArticlePath').replace('$1', '') + '([^#?]+)');
	const redlinkRX = new RegExp(mw.config.get('wgScript') + '\\?title=([^#&]+)');
	$('a', $content).each(function () {
		if (!$(this).attr('href')) {
			// Ignore if the <a> doesn't have a href
			return;
		}
		let articleTitleReMatch = articleRX.exec($(this).attr('href').toString());
		if (!articleTitleReMatch) {
			// Try the redlink check
			articleTitleReMatch = redlinkRX.exec($(this).attr('href').toString());
			if (!articleTitleReMatch) {
				return;
			}
		}
		let pgTitle;
		try {
			pgTitle = decodeURIComponent(articleTitleReMatch[1]).replace(/_/g, ' ');
		} catch (error) {
			// Happens sometimes on non-username paths, like if there's a slash in the path
			return;
		}
		const userTitleReMatch = userTitleRX.exec(pgTitle);
		if (!userTitleReMatch) {
			return;
		}
		let username = userTitleReMatch[2];
		// Ensure consistent case conversions with PHP as per https://phabricator.wikimedia.org/T292824
		username = new mw.Title(username).getMainText();
		if (!mw.util.isIPAddress(username, true)) {
			if (!userLinks.get(username)) {
				userLinks.set(username, []);
			}
			userLinks.get(username).push($(this));
		}
	});
	return userLinks;
}

const fetchedUserInfo = new Map();
const lockLogIdsToUsers = new Map();

/**
 * Check whether a user is locked and if they are, return details about it
 *
 * @param {string[]} users Usernames to check
 *
 * @return {Promise}
 */
async function lockedUsers_getLockBatch(users) {
	for (const user of users) {
		fetchedUserInfo.set(user, { locked: false, tooltip: '' });
	}

	// Pre-check whether they're locked at all - if no, return early
	try {
		const response = await localApi.get({
			action: 'query',
			list: 'globalusers',
			gususers: users,
			gusprop: 'locked'
		});
		for (const userInfo of response.query.globalusers) {
			// If the 'locked' field is present, then the user is locked
			if ('locked' in userInfo) {
				fetchedUserInfo.set(userInfo.name, { locked: true, tooltip: '' });
				if ('locklogid' in userInfo) {
					lockLogIdsToUsers.set(userInfo.locklogid, userInfo.name);
				}
			}
		}
	} catch (error) {}
}

async function lockedUsers_getLockReasonsBatch(logIds) {
	try {
		const response = await metaApi.get({
			action: 'query',
			list: 'logevents',
			leprop: 'ids|user|timestamp|comment|details',
			leids: logIds,
			lelimit: 'max'
		});

		for (let logEvent of response.query.logevents) {
			const timestamp = new Date(logEvent.timestamp);
			const prettyTimestamp = lockedUsers_formatTimeSince(timestamp);
			const user = lockLogIdsToUsers.get(logEvent.logid);
			const tooltip = `Locked by ${logEvent.user}: ${logEvent.comment} (${prettyTimestamp} ago)`;
			fetchedUserInfo.set(user, { locked: true, tooltip });
		}
	} catch (error) {}
}

/**
 * Formats time since a date. Taken from mark-blocked.js
 *
 * @param {targetDate} Date to check the time since for
 *
 * @return {string} A prettified string regarding time since the lock occured
 */
function lockedUsers_formatTimeSince(targetDate) {
	const lockedUsers_padNumber = (number) => number <= 9 ? '0' + number : number;

	const msSince = new Date() - targetDate;

	let minutes = Math.floor(msSince / 60000);
	if (!minutes) {
		return Math.floor(msSince / 1000) + 's';
	}

	let hours = Math.floor(minutes / 60);
	minutes %= 60;

	let days = Math.floor(hours / 24);
	hours %= 24;
	if (days) {
		return `${days}${(days < 10 ? '.' + lockedUsers_padNumber(hours) : '' )}d`;
	}
	return `${hours}:${lockedUsers_padNumber(minutes)}`;
}

// On window load, get all the users on the page and check if they're blocked
$.when( $.ready, mw.loader.using( 'mediawiki.util' ) ).then( function () {
	mw.hook('wikipage.content').add(async function ($content) {
		const usersOnPage = lockedUsers_getUsers($content);

		const batchSizeLocal = (await localApi.getUserInfo()).rights.includes('apihighlimits') ? 500 : 50;
		const usersOnPageList = [...usersOnPage.keys()];
		while (usersOnPageList.length) {
			await lockedUsers_getLockBatch(usersOnPageList.splice(0, batchSizeLocal));
		}

		const batchSizeMeta = (await metaApi.getUserInfo()).rights.includes('apihighlimits') ? 500 : 50;
		const logLockIdsList = [...lockLogIdsToUsers.keys()];
		while (logLockIdsList.length) {
			await lockedUsers_getLockReasonsBatch(logLockIdsList.splice(0, batchSizeMeta));
		}

		usersOnPage.forEach((val, key, _) => {
			const { locked, tooltip } = fetchedUserInfo.get(key.replaceAll('_', ' '));
			if (locked) {
				val.forEach(($link) => {
					$link.css({ opacity: 0.4, 'border-bottom-size': 'thick', 'border-bottom-style': 'dashed', 'border-bottom-color': 'red' });
					$link.attr('title', tooltip);
				});
			}
		});
	});
});
})();
// </nowiki>