User:Mfield/Scripts/RelativeTimestamps.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.
//Script to add human relative times after wiki timestamps - assumes UTC
function updateRecentHumanDiffs(){ //updates an timestamps of less than an hour every minute
	$('.mf-reltime.mf-recent').each(function(){
		let oldmin = parseInt($(this).text().replace(/[^0-9]/g,''));
		if (oldmin==59){
			$(this).html(' (1 hr. ago)').removeClass('mf-recent');
			return;
		}
		$(this).html(` (${oldmin+1} min. ago)`);
	});
}
	
$(function(){
	
	//only run in view or history modes
	if (!['view', 'history'].includes(mw.config.values.wgAction)){
		return false;
	}

	var style = $(`<style>
						.mf-reltime { 
							font-size: 0.8em; 
							display: inline-block; 
							margin-left: 5px; 
							min-width: 75px;
							position: relative;
							top: -1px;
						} 
						.mw-enhanced-rc .mf-reltime {
							margin-left: 0px;
							margin-right: 5px;
							font-family: sans-serif;
						}
						.mf-recent {
							font-weight: bold;
						}
						.mw-enhanced-rc .mf-reltime.mf-recent {
							font-weight: normal;
						}
					</style>`);
	$('html > head').append(style);

	var timestampUpdateTimer;
	
	Date.prototype.humanDiff = function () {
    	const diff_ms = this - new Date();
    	const diff_m = diff_ms / 60000;
    	const timeScalars = [60, 24, 7, 4.345, 12];
    	const timeUnits = ['minute', 'hour', 'day', 'week', 'month', 'year'];
    	let timeScalarIndex = 0, scaledTime = diff_m;
    	while (Math.abs(scaledTime) > timeScalars[timeScalarIndex]) {
        	scaledTime /= timeScalars[timeScalarIndex++];
    	}
    	const rtf = new Intl.RelativeTimeFormat("en", { style: "short" });
    	return rtf.format(scaledTime.toFixed(0), timeUnits[timeScalarIndex]);
	};

	function formatDateFromVariousWikiDateFormats(unknown){
		let offset = mw.user.options.get( 'timecorrection' ).split( '|' )[ 1 ]/60;
		let offset_text = (offset < 0 ? '-' : '+')+String(Math.abs(offset)).padStart(2,0)+':00';
		if (unknown.indexOf('UTC')>-1) offset_text = '+00:00';
		let rgx = new RegExp(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d/)
		if (unknown.match(rgx)) return unknown+offset_text;
		const months = ['','January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
		const time_unformatted = unknown.split(', ');
		var d;
		var y;
		var month;
		var hm = time_unformatted[0];
		if (time_unformatted.length==3) { //then we have HH:MM, DD Month, YYYY
			let dM = time_unformatted[1].split(' ');
			d = dM[0];
			month = dM[1];
			y = time_unformatted[2];
		}
		else if (time_unformatted.length==2){ //else we have HH:MM, DD Month YYYY or HH:MM, YYYY Month DD
			let xMx = time_unformatted[1].split(' ');
			month = xMx[1];
			if (xMx[0].length<3){
				d = xMx[0];
				y = xMx[2];
			}
			else {
				y = xMx[0];
				d = xMx[2];	
			}
		}
		else {
			console.log(unknown);
			return false; //unexpected format
		}
		let m = months.indexOf(month);
		let mm = (m<10 ? '0' : '')+m.toString();
		let dd = (d.length<2 ? '0' : '')+d;
		let date = y+'-'+mm+'-'+dd+'T'+hm+':00'+offset_text;	
		if (date.match(rgx)) return date;
		return false;
	}

	function addWatchlistTimeDiffs(){
		$('.mf-reltime').remove();
		$('table.mw-changeslist-line').each(function(){
			let ts = $(this).attr('data-mw-ts');
			if (!ts) return;
			let datetime = ts.slice(0,4)+'-'+ts.slice(4,6)+'-'+ts.slice(6,8)+'T'+ts.slice(8,10)+':'+ts.slice(10,12)+':'+ts.slice(12,14);
			let unixtime = Date.parse(datetime);
			let diff = new Date(datetime).humanDiff();
			let recent = ( Date.now()-unixtime < 3600000 ? ' mf-recent' : '');
			$(this).children('tbody').children('tr').children('td.mw-enhanced-rc').append(`<span  ts="${unixtime}" class="mf-reltime${recent}">(${diff})</span>`);
		});
	}

	function addOtherPagesTimeDiffs(){
		$('a.ext-discussiontools-init-timestamplink, a.mw-changeslist-date, li.mw-logline-block>a').each(function(){
			let date = formatDateFromVariousWikiDateFormats($(this).html());
			if (!date) return;
			try {
				let unixtime = Date.parse(date);
				let diff = new Date(date).humanDiff().replace(/\s/g, '&nbsp;');	
				let recent = ( Date.now()-unixtime < 3600000 ? ' mf-recent' : '');
				$(this).append(`<span ts="${unixtime}" class="mf-reltime${recent}">(${diff})</span>`);
			}
			catch(error){
				console.error(error);
			}
		});
	}

	function bindShowNewChanges(){
		$(document).on( "ajaxComplete", function(event, request, settings) {
			if (settings.url=='/w/api.php?format=json&action=query'){
				addWatchlistTimeDiffs();
				clearInterval(timestampUpdateTimer);
				timestampUpdateTimer = setInterval(updateRecentHumanDiffs, 60000);
			}
		});
	}

	if (mw.config.values.wgPageName == "Special:Watchlist"){
		addWatchlistTimeDiffs();
		setTimeout(bindShowNewChanges, 10000);
	}
	else {
		addOtherPagesTimeDiffs();
	}
	
	if (!$('.mf-reltime.mf-recent').length) return;

	timestampUpdateTimer = setInterval(updateRecentHumanDiffs, 60000);
	
});