User:Mxn/CommentsInLocalTime.js

/**

* Comments in local time

* User:Mxn/CommentsInLocalTime

*

* Adjust timestamps in comment signatures to use easy-to-understand, relative

* local time instead of absolute UTC time.

*

* Inspired by Wikipedia:Comments in Local Time.

*

* @author User:Mxn

*/

/**

* Default settings for this gadget.

*/

window.LocalComments = $.extend({

// USER OPTIONS ////////////////////////////////////////////////////////////

/**

* When false, this gadget does nothing.

*/

enabled: true,

/**

* Formats to display inline for each timestamp, keyed by a few common

* cases.

*

* If a property of this object is set to a string, the timestamp is

* formatted according to the documentation at

* .

*

* If a property of this object is set to a function, it is called to

* retrieve the formatted timestamp string. See

* for the various things you can

* do with the passed-in moment object.

*/

formats: {

/**

* Within a day, show a relative time that’s easy to relate to.

*/

day: function (then) { return then.fromNow(); },

/**

* Within a week, show a relative date and specific time, still helpful

* if the user doesn’t remember today’s date. Don’t show just a relative

* time, because a discussion may need more context than “Last Friday”

* on every comment.

*/

week: function (then) { return then.calendar(); },

/**

* The calendar() method uses an ambiguous “MM/DD/YYYY” format for

* faraway dates; spell things out for this international audience.

*/

other: function (then) {

var pref = mw.user.options.values.date;

return then.format(window.LocalComments.formatOptions[pref] || "LLL");

},

},

/**

* Formats to display in each timestamp’s tooltip, one per line.

*

* If an element of this array is a string, the timestamp is formatted

* according to the documentation at

* .

*

* If an element of this array is a function, it is called to retrieve the

* formatted timestamp string. See

* for the various things you can do with the passed-in moment object.

*/

tooltipFormats: [

function (then) { return then.fromNow(); },

"LLLL",

"YYYY-MM-DDTHH:mmZ",

],

/**

* When true, this gadget refreshes timestamps periodically.

*/

dynamic: true,

}, {

// SITE OPTIONS ////////////////////////////////////////////////////////////

/**

* Numbers of namespaces to completely ignore. See Wikipedia:Namespace.

*/

excludeNamespaces: [-1, 0, 8, 100, 108, 118],

/**

* Names of tags that often directly contain timestamps.

*

* This is merely a performance optimization. This gadget will look at text

* nodes in any tag other than the codeTags, but adding a tag here ensures

* that it gets processed the most efficient way possible.

*/

proseTags: ["dd", "li", "p", "td"],

/**

* Names of tags that don’t contain timestamps either directly or

* indirectly.

*/

codeTags: ["code", "input", "pre", "textarea"],

/**

* An object mapping the date format user options provided by this MediaWiki

* installation to corresponding Moment.js format strings. The user can

* choose a preferred date format in

* Special:Preferences#mw-prefsection-rendering-dateformat. See

* mw:Manual:Date formatting. These formats determine the default

* timestamp display format.

*

* These formats come from

* .

* When customizing these formats for a different wiki’s content language,

* consult the language’s corresponding message file’s `$dateFormats`

* variable. Use only the messages with the “both” suffix, and remove that

* suffix from each key. The MediaWiki date format syntax is described in

*

* and mw:Help:Extension:ParserFunctions##time. The Moment.js syntax is

* described in .

*

* @todo Automatically convert MediaWiki date format syntax to Moment.js

* date format syntax.

*/

formatOptions: {

mdy: "HH:mm, MMMM D, YYYY", // H:i, F j, Y

dmy: "HH:mm, D MMMM YYYY", // H:i, j F Y

ymd: "HH:mm, YYYY MMMM D", // H:i, Y F j

"ISO 8601": "YYYY-MM-DDTHH:mm:ss", // xnY-xnm-xnd"T"xnH:xni:xns

},

/**

* Expected format or formats of the timestamps in existing wikitext. If

* very different formats have been used over the course of the wiki’s

* history, specify an array of formats.

*

* This option expects parsing format strings

* .

*/

parseFormat: "H:m, D MMM YYYY",

/**

* Regular expression matching all the timestamps inserted by this MediaWiki

* installation over the years. This regular expression should more or less

* agree with the parseFormat option.

*

* Until 2005:

* 18:16, 23 Dec 2004 (UTC)

* 2005–present:

* 08:51, 23 November 2015 (UTC)

*/

parseRegExp: /\d\d:\d\d, \d\d? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w* \d{4} \(UTC\)/,

/**

* UTC offset of the wiki's default local timezone. See

* mw:Manual:Timezone.

*/

utcOffset: 0,

}, window.LocalComments);

$(function () {

if (!LocalComments.enabled

|| LocalComments.excludeNamespaces.indexOf(mw.config.get("wgNamespaceNumber")) !== -1

|| ["view", "submit"].indexOf(mw.config.get("wgAction")) === -1

|| mw.util.getParamValue("disable") === "loco")

{

return;

}

var proseTags = LocalComments.proseTags.join("\n").toUpperCase().split("\n");

// Exclude

var codeTags = $.merge(LocalComments.codeTags, ["time"]).join(", ");

// Look in the content body for DOM text nodes that may contain timestamps.

// The wiki software has already localized other parts of the page.

var root = $("#wikiPreview, #mw-content-text")[0];

if (!root || !("createNodeIterator" in document)) return;

var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, {

acceptNode: function (node) {

// We can’t just check the node’s direct parent, because templates

// like Template:Talkback and Template:Resolved may place a

// signature inside a nondescript .

var isInProse = proseTags.indexOf(node.parentElement.nodeName) !== -1

|| !$(node).parents(codeTags).length;

var isDateNode = isInProse && LocalComments.parseRegExp.test(node.data);

return isDateNode ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;

},

});

/**

* Marks up each timestamp found.

*/

function wrapTimestamps() {

var prefixNode;

while ((prefixNode = iter.nextNode())) {

var result = LocalComments.parseRegExp.exec(prefixNode.data);

if (!result) continue;

// Split out the timestamp into a separate text node.

var dateNode = prefixNode.splitText(result.index);

var suffixNode = dateNode.splitText(result[0].length);

// Determine the represented time.

var then = moment.utc(result[0], LocalComments.parseFormat);

if (!then.isValid()) {

// Many Wikipedias started out with English as the default

// localization, so fall back to English.

then = moment.utc(result[0], "H:m, D MMM YYYY", "en");

}

// Wrap the timestamp inside a

// This loop must wrap the text in a

// even if the time is invalid, to avoid an infinite loop as the

// same node keeps coming up as a candidate that the node iterator

// thinks is valid.

// User talk:Mxn/CommentsInLocalTime.js#Interface-protected edit request on 18 November 2022

var timeElt = $("

if (then.isValid()) {

then.utcOffset(-LocalComments.utcOffset);

// MediaWiki core styles .explain[title] the same way as

// abbr[title], guiding the user to the tooltip.

timeElt.addClass("localcomments explain");

timeElt.attr("datetime", then.toISOString());

}

$(dateNode).wrap(timeElt);

}

}

/**

* Returns a formatted string for the given moment object.

*

* @param {Moment} then The moment object to format.

* @param {String} fmt A format string or function.

* @returns {String} A formatted string.

*/

function formatMoment(then, fmt) {

return (fmt instanceof Function) ? fmt(then) : then.format(fmt);

}

/**

* Reformats a timestamp marked up with the

*

* @param {Number} idx Unused.

* @param {Element} elt The

*/

function formatTimestamp(idx, elt) {

var iso = $(elt).attr("datetime");

var then = moment(iso, moment.ISO_8601);

var now = moment();

var withinHours = Math.abs(then.diff(now, "hours", true))

<= moment.relativeTimeThreshold("h");

var formats = LocalComments.formats;

var text;

if (withinHours) {

text = formatMoment(then, formats.day || formats.other);

}

else {

var dayDiff = then.diff(moment().startOf("day"), "days", true);

if (dayDiff > -6 && dayDiff < 7) {

text = formatMoment(then, formats.week || formats.other);

}

else text = formatMoment(then, formats.other);

}

$(elt).text(text);

// Add a tooltip with multiple formats.

elt.title = $.map(LocalComments.tooltipFormats, function (fmt, idx) {

return formatMoment(then, fmt);

}).join("\n");

// Register for periodic updates.

var withinMinutes = withinHours

&& Math.abs(then.diff(now, "minutes", true))

<= moment.relativeTimeThreshold("m");

var withinSeconds = withinMinutes

&& Math.abs(then.diff(now, "seconds", true))

<= moment.relativeTimeThreshold("s");

var unit = withinSeconds ? "seconds" :

(withinMinutes ? "minutes" :

(withinHours ? "hours" : "days"));

$(elt).attr("data-localcomments-unit", unit);

}

/**

* Reformat all marked-up timestamps and start updating timestamps on an

* interval as necessary.

*/

function formatTimestamps() {

wrapTimestamps();

$(".localcomments").each(function (idx, elt) {

// Update every timestamp at least this once.

formatTimestamp(idx, elt);

if (!LocalComments.dynamic) return;

// Update this minute’s timestamps every second.

if ($("[data-localcomments-unit='seconds']").length) {

setInterval(function () {

$("[data-localcomments-unit='seconds']").each(formatTimestamp);

}, 1000 /* ms */);

}

// Update this hour’s timestamps every minute.

setInterval(function () {

$("[data-localcomments-unit='minutes']").each(formatTimestamp);

}, 60 /* s */ * 1000 /* ms */);

// Update today’s timestamps every hour.

setInterval(function () {

$("[data-localcomments-unit='hours']").each(formatTimestamp);

}, 60 /* min */ * 60 /* s */ * 1000 /* ms */);

});

}

mw.loader.using("moment", function () {

wrapTimestamps();

formatTimestamps();

});

LocalComments.wrapTimestamps = wrapTimestamps;

LocalComments.formatTimestamps = formatTimestamps;

});