User:Thparkth/CommentsInLocalTime.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Thparkth/CommentsInLocalTime. |
/**
* 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
* <http://momentjs.com/docs/#/displaying/format/>.
*
* If a property of this object is set to a function, it is called to
* retrieve the formatted timestamp string. See
* <http://momentjs.com/docs/#/displaying/> 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
* <http://momentjs.com/docs/#/displaying/format/>.
*
* If an element of this array is a function, it is called to retrieve the
* formatted timestamp string. See <http://momentjs.com/docs/#/displaying/>
* 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
* <https://doc.wikimedia.org/mediawiki-core/1.34.0/php/MessagesEn_8php.html#a2fc93ea5327f655d3ed306e221ee33f0>.
* 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
* <https://doc.wikimedia.org/mediawiki-core/1.34.0/php/classLanguage.html#a94f84f82d7f954c4cb2e191d22c6e6a6>
* and [[mw:Help:Extension:ParserFunctions##time]]. The Moment.js syntax is
* described in <https://momentjs.com/docs/#/parsing/string-format/>.
*
* @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
* <http://momentjs.com/docs/#/parsing/string-format/>.
*/
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,(?:\s[1-9]\s|\s0[1-9]\s|\s1[0-9]\s|\s2[0-9]\s|\s3[0-1]\s)(?: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 <time> to avoid an infinite loop when iterating over text nodes.
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 <span>.
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;
var loopEscapeCounter = 0; // we'll break out if the while loop runs more than 5,000 times to avoid a browser hang
while ((prefixNode = iter.nextNode())) {
if (loopEscapeCounter++ > 5000) { // escape infinite loop
console.log("ERROR: CommentsInLocalTime.js loop breakout in wrapTimestamps()");
break;
}
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");
}
if (!then.isValid()) continue;
then.utcOffset(-LocalComments.utcOffset);
// Wrap the timestamp inside a <time> element for findability.
var timeElt = $("<time />");
// 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 <time> element.
*
* @param {Number} idx Unused.
* @param {Element} elt The <time> element.
*/
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();
});
});