User:A455bcd9/nominations viewer.js

//

// Nominations Viewer

//

// Description: Compact nominations for WP:FAC, WP:FAR, WP:FLC,

// WP:FLRC, WP:FPC, and WP:PR.

// Documentation: Wikipedia:Nominations Viewer

//

// ===

//

// Settings

// ---

//

// Default:

//

// NominationsViewer =

// {

// 'enabledPages': ['Wikipedia:Featured article candidates', ...],

// 'nominationData': ['images', 'age', 'nominators', 'participants', 'votes'],

// }

$(() => {

// Check the URL to determine if this script should be disabled.

if (window.location.href.includes('&disable=nomviewer')) {

return;

}

// Check if already ran elsewhere.

if (window.nominationsViewer) {

return;

}

window.nominationsViewer = true;

const NominationsViewer = window.NominationsViewer || {};

if (!NominationsViewer.enabledPages) {

NominationsViewer.enabledPages = {

'User:Gary/sandbox': 'nominations',

'Wikipedia:Featured article candidates': 'nominations',

'Wikipedia:Featured article review': 'reviews',

'Wikipedia:Featured list candidates': 'nominations',

'Wikipedia:Featured list removal candidates': 'reviews',

'Wikipedia:Featured picture candidates': 'pictures',

'Wikipedia:Peer review': 'peer reviews',

};

}

if (!NominationsViewer.nominationData) {

NominationsViewer.nominationData = [

'images',

'age',

'lastedit',

'nominators',

'participants',

'votes',

];

}

/**

* Add empty nomination data holders for a nomination.

*

* @param {string} pageName Name of the nomination page.

* @param {jQuery} $parentNode Parent node containing the entire nomination.

* @param {Array} ids The ID names to create.

* @returns {jQuery} The new node we added.

*/

function addNominationData(pageName, $parentNode, ids) {

return ids.map((id) => {

const $span = $(``);

return $parentNode

.children()

.last()

.before($span);

});

}

function addAllNomInfo($headings) {

const data = { allH3Length: $headings.length };

const $expandAllLink = $(

'expand all'

).on('click', data, expandAllNoms);

const $collapseAllLink = $(

'collapse all'

).on('click', data, collapseAllNoms);

const $info = $('')

.append(' (')

.append($expandAllLink)

.append(' / ')

.append($collapseAllLink)

.append(')');

return $headings

.first()

.next()

.prevUntil('h2')

.last()

.prev()

.append($info);

}

/**

* Call the Wikipedia API with params then run a function on the return data.

*

* @param {Object} params The params to pass to the Wikipedia API.

* @param {Function} callback The function to run with the return data.

* @returns {undefined}

*/

function addNomData(params, callback) {

$.getJSON(mw.util.wikiScript('api'), {

format: 'json',

...params,

})

.done(callback)

.fail(() => {});

}

/**

* Add all data to a nomination.

*

* @param {string} pageName The page name.

* @returns {undefined}

*/

function addAllNomData(pageName) {

// Participants, age. Get all the edits for this nomination.

addNomData(

{

action: 'query',

prop: 'revisions',

rvdir: 'newer',

rvlimit: 500,

titles: pageName,

},

allRevisionsCallback

);

// Images, nominators, votes. Get the contents of the latest version of this

// nomination.

addNomData(

{

action: 'query',

prop: 'revisions',

rvdir: 'older',

rvlimit: 1,

rvprop: 'content',

titles: pageName,

},

currentRevisionCallback

);

}

/**

* Add data to a nomination.

*

* @param {Object} options Options

* @param {string} options.pageName The page name to which to add this data.

* @param {string} options.data The data to add.

* @param {string} options.id The ID of the field to add to.

* @param {string} options.hoverText Data that appears on hover.

* @returns {undefined}

*/

function addNewNomData({ pageName, data, id, hoverText }) {

if (!data) {

return;

}

// Select the element we want to add values to.

const $id = $(`#${id}-${simplifyPageName(pageName)}`);

const $newChild = $('');

const $abbr = $(`${data}`);

$newChild.append($abbr);

$id.append($newChild);

}

/**

* Create the data that appears next to the nomination's listing.

*

* @param {string} pageName Page name of the nomination page.

* @returns {jQuery} The new node we added.

*/

function createData(pageName) {

const $newSpan = $('').append(

'('

);

const matchArchiveNumber = pageName.match(/(\d+)$/);

const conditions = matchArchiveNumber && matchArchiveNumber[1] > 1;

const matchArchiveNumberPrint = (() => {

if (conditions) {

const number = parseInt(matchArchiveNumber[1], 10);

const ordinalSuffix = (() => {

switch (number) {

case 2:

return 'nd';

case 3:

return 'rd';

default:

return 'th';

}

})();

return `: ${number}${ordinalSuffix}`;

}

return '';

})();

const $viewLink = $(

`nomination\

${matchArchiveNumberPrint}`

);

return $newSpan.append($viewLink).append(')');

}

function createNewNode({ oldNode, showHideLink, newSpan, index }) {

const $newNode = $(`

`).append(

oldNode.clone(true)

);

const $heading = $newNode.children().first();

$heading

.prepend(`${index + 1}. `)

.append(' ')

.append(showHideLink)

.append(newSpan);

return $newNode;

}

/**

* Replace a nomination with a new and improved one.

*

* @param {Object} options Options

* @param {jQuery} options.$h3 The h3 heading of the nomination.

* @param {number} options.index The index of the nomination among the

* others.

* @returns {undefined}

*/

function createNomination({ $h3, index }) {

// Get edit links. It has to be an edit link, and not an article link,

// because it has to point to the nomination page, not the article.

const $editLinks = $h3.find('.mw-editsection a');

const useParentDiv = $editLinks.length === 0;

const parentDiv = $h3.parent();

const $editLinks2 = parentDiv.find('.mw-editsection a');

const $editLinksOption = useParentDiv ? $editLinks2 : $editLinks;

// There are no edit links.

if ($editLinksOption.length === 0) {

return;

}

const titleRegex = /[&?]title=(.*?)(?:&|$)/;

// Find the edit link that matches our regex.

const $filteredEditLinks = $editLinksOption.filter((elementIndex, element) =>

$(element)

.attr('href')

.match(titleRegex)

);

// Only continue if there are filtered edit links. They won't appear when a

// Peer Review is "too long" and therefore is replaced with a message to go

// to the review page directly. So, skip this nomination.

if (

$filteredEditLinks.length === 0 ||

!$filteredEditLinks.eq(0).attr('href') ||

!$filteredEditLinks

.eq(0)

.attr('href')

.match(titleRegex)

) {

return;

}

// Get the name of the nomination page.

const pageName = decodeURIComponent(

$filteredEditLinks

.eq(0)

.attr('href')

.match(titleRegex)[1]

);

// Create the [show] / [hide] link.

const showHideLink = createShowHideLink(index);

// Create the spot to put the data that we will retrieve via the Wikipedia

// API.

const newSpan = createData(pageName);

// Move the nomination into a hidden node.

hideNomination($h3, index);

// Add placeholders for the data that we will retrieve for the API.

addNominationData(pageName, newSpan, NominationsViewer.nominationData);

const nodeToReplace = useParentDiv ? parentDiv : $h3;

// Create the nomination's title line.

const newNode = createNewNode({

oldNode: nodeToReplace,

showHideLink,

newSpan,

index,

});

// Create the actual nomination

const nomDiv = generateNomination(index, newNode, nodeToReplace);

// Replace this nomination with the new one we created.

nodeToReplace.replaceWith(nomDiv);

// Ask the API to add data to our placeholders.

addAllNomData(pageName);

}

function createShowHideLink(index) {

const span = $('');

const link = $(`show`).on(

'click',

{ index },

toggleNomClick

);

return span

.append('[')

.append(link)

.append(']');

}

function generateNomination(index, newNode, oldNode) {

return $(`

`)

.append(newNode.clone(true))

.append($(oldNode[0].nextSibling).clone(true));

}

// This function MUST stay in JavaScript, rather than switch to jQuery, for

// optmization reasons.

//

// The jQuery version slowed the page down by about 28%. This version slows

// the page down by about 11%, so it is about 17% faster.

function hideNomination($h3, index) {

// Re-create all nodes between this H3 node, and the next one, then place it

// into a new node.

const hiddenNode = document.createElement('div');

hiddenNode.className = 'nomination-body';

hiddenNode.id = `nom-data-${index}`;

hiddenNode.style.display = 'none';

let parentNode = $h3[0].parentNode;

let sectionStart = parentNode.classList.contains('mw-heading3') ? parentNode : $h3[0];

let nomNextSibling = sectionStart.nextSibling;

// Continue to the next node, as long as the next node still exists, it

// isn't an H2 or H3, and it doesn't have the class "printfooter or mw-heading2"

while (

nomNextSibling &&

!(

['H2', 'H3'].includes(nomNextSibling.nodeName) ||

(

nomNextSibling.childNodes &&

nomNextSibling.childNodes.length > 1 &&

['H2', 'H3'].includes(nomNextSibling.childNodes[1].nodeName)

)

) &&

!(

nomNextSibling.classList &&

nomNextSibling.classList.contains('printfooter')

) &&

!(

nomNextSibling.classList &&

nomNextSibling.classList.contains('mw-heading2')

) &&

!(

nomNextSibling.classList &&

nomNextSibling.classList.contains('mw-heading3')

)

) {

const nomNextSiblingTemporary = nomNextSibling.nextSibling;

// Move the node, if it isn't a text node

if (nomNextSibling.nodeType !== 3) {

// eslint-disable-next-line unicorn/prefer-node-append

hiddenNode.appendChild(nomNextSibling);

}

nomNextSibling = nomNextSiblingTemporary;

}

// Insert hidden content

return sectionStart.after(hiddenNode);

}

/**

* The main function, to run the script.

*

* @returns {undefined}

*/

function init() {

let currentPageIsASubpage;

let currentPageIsEnabled;

const pageName = mw.config.get('wgPageName');

// Check if enabled on this page

Object.keys(NominationsViewer.enabledPages).forEach((page) => {

if (pageName === page.replace(/\s/g, '_')) {

currentPageIsEnabled = true;

} else if (pageName.startsWith(page.replace(/\s/g, '_'))) {

currentPageIsASubpage = true;

}

});

if (

!currentPageIsEnabled ||

mw.config.get('wgAction') !== 'view' ||

window.location.href.includes('&oldid=') ||

currentPageIsASubpage

) {

return;

}

// Append the CSS now, since we're definitely running the script on this

// page.

addCss();

const $parentNode = $('.mw-content-ltr');

const $h3s = $parentNode.find('h3');

addAllNomInfo($h3s);

// Loop through each nomination

$h3s.each((index, element) =>

createNomination({

$h3: $(element),

index,

})

);

// Fix any conflicts with collapsed comments (using the special template).

$('.collapseButton').each((index, element) => {

const $link = $(element)

.children()

.first();

// eslint-disable-next-line unicorn/prefer-string-slice

const newIndex = $link

.attr('id')

.substring(

$link.attr('id').indexOf('collapseButton') + 'collapseButton'.length,

$link.attr('id').length

);

$link.attr('href', '#').on('click', { newIndex }, collapseTable);

});

}

// Helpers

function collapseTable(event) {

event.preventDefault();

const tableIndex = event.data.index;

const collapseCaption = 'hide';

const expandCaption = 'show';

const $button = $(`#collapseButton${tableIndex}`);

const $table = $(`#collapsibleTable${tableIndex}`);

if ($table.length === 0 || $button.length === 0) {

return false;

}

const $rows = $table.find('> tbody > tr');

if ($button.text() === collapseCaption) {

$rows.each((index, element) => {

if (index === 0) {

return true;

}

return $(element).hide();

});

return $button.text(expandCaption);

}

$rows.each((index, element) => {

if (index === 0) {

return true;

}

return $(element).show();

});

return $button.text(collapseCaption);

}

// Add CSS to the page, to use for this script. This is a separate function,

// so that it's more easy to disable it when necessary.

function addCss() {

mw.util.addCSS(`

#content .nomination h3 {

margin-bottom: 0;

padding-top: 0;

}

.nomination-data,

.nomination-order,

.overall-controls {

font-size: 75%;

font-weight: normal;

}

.nomination-order {

display: inline-block;

width: 25px;

}

.nomv-show-hide {

display: inline-block;

font-size: 13px;

font-weight: normal;

margin-right: 2.5px;

width: 40px;

}

.nomv-show-hide a {

display: inline-block;

text-align: center;

width: 31px;

}

.nomv-data::before {

content: " · ";

}

.nomv-data abbr {

white-space: nowrap;

}

`);

}

function expandAllNoms(event) {

return toggleAllNoms(event, 'expand');

}

function collapseAllNoms(event) {

return toggleAllNoms(event, 'collapse');

}

function toggleAllNoms(event, actionParam) {

let action = actionParam;

if (!action) {

action = 'expand';

}

event.preventDefault();

const { allH3Length } = event.data;

new Array(allH3Length).fill().forEach((value, index) => {

toggleNom(index, action);

});

}

function toggleNom(id, actionParam) {

let action = actionParam;

if (!action) {

action = '';

}

const toggleHideNom = ($node, $nomButton) => {

$node.hide();

return $nomButton.text('show');

};

const toggleShowNom = ($node, $nomButton) => {

$node.show();

return $nomButton.text('hide');

};

const $node = $(`#nom-data-${id}`);

const $nomButton = $(`#nom-button-${id}`);

// These are actions that override the status for all nominations.

if (action === 'collapse') {

return toggleHideNom($node, $nomButton);

}

if (action === 'expand') {

return toggleShowNom($node, $nomButton);

}

// These have to be separate from the above because they have a lower

// priority.

if ($node.is(':visible')) {

return toggleHideNom($node, $nomButton);

}

if ($node.is(':hidden')) {

return toggleShowNom($node, $nomButton);

}

return null;

}

function toggleNomClick(event) {

event.preventDefault();

const { index } = event.data;

return toggleNom(index);

}

// Callbacks

function addParticipants(revisions, pageName, queryContinue) {

if (!dataIsEnabled('participants') || !revisions) {

return;

}

const users = {};

let userCount = 0;

revisions.forEach((revision) => {

if (!revision.user) {

return;

}

if (users[revision.user]) {

users[revision.user] += 1;

} else {

users[revision.user] = 1;

userCount += 1;

}

});

const moreThan = queryContinue ? 'more than ' : '';

const usersArray = Object.keys(users).map((user) => [

user,

parseInt(users[user], 10),

]);

const usersArray2 = [...usersArray]

.sort((a, b) => {

if (a[1] < b[1]) {

return 1;

}

if (a[1] > b[1]) {

return -1;

}

return 0;

})

.map((user) => `${user[0]}: ${user[1]}`);

addNewNomData({

pageName,

data: `${moreThan + userCount} ${pluralize('participant', userCount)}`,

id: 'participants',

hoverText: `Sorted from most to least edits Total edits: ${

revisions.length

} Format: "editor: \

number of edits": ${usersArray2.join(' ')}`,

});

}

function allRevisionsCallback(object) {

const vars = formatJSON(object);

if (!vars) {

return;

}

// Participants

addParticipants(vars.revisions, vars.pageName, object['query-continue']);

// Nomination age

addAge(vars.firstRevision, vars.pageName);

// Last edit

addLastEdit(vars.lastRevision, vars.pageName);

}

function addImagesCount(content, pageName) {

if (!nomType('pictures') || !dataIsEnabled('images')) {

return;

}

// Determine number of images in the nomination

const pattern1 = /\[\[(file|image):.*?]]/gi;

const pattern2 = /\n(file|image):.*\|/gi;

const matches1 = content.match(pattern1);

const matches2 = content.match(pattern2);

const matches = matches1 || matches2 || [];

const images = matches.map((match) => {

const split = match.split('|');

const filename = $.trim(split[0].replace(/^\[\[/, ''));

return filename;

});

addNewNomData({

pageName,

data: `${matches.length} ${pluralize('image', matches.length)}`,

id: 'images',

hoverText: `Images (in order of appearance): ${images.join(

' '

)}`,

});

}

function getNominators(content) {

let nomTypeText = '';

let listOfNominators = {};

switch (nomType()) {

case 'nominations':

nomTypeText = 'nominator';

listOfNominators = findNominators(content, /Nominator(\(s\))?:.*/);

// No nominators were found, so try once more with a different pattern.

if ($.isEmptyObject(listOfNominators)) {

listOfNominators = findNominators(content, /:''.*/);

}

break;

case 'reviews':

nomTypeText = 'notification';

listOfNominators = findNominators(content, /(Notified|Notifying):?.*/);

break;

case 'pictures':

nomTypeText = 'nominator';

listOfNominators = findNominators(

content,

/\* Support as nominator – .*/

);

break;

default:

}

return { listOfNominators, nomTypeText };

}

function addNominators(content, pageName) {

if (!dataIsEnabled('nominators') || nomType('peer reviews')) {

return;

}

const { listOfNominators, nomTypeText } = getNominators(content);

let allNominators = Object.keys(listOfNominators)

.map((n) => n)

.sort();

let data;

if (allNominators.length > 0) {

data = `${allNominators.length} ${pluralize(

nomTypeText,

allNominators.length

)}`;

// We couldn't identify any nominators.

} else {

// Use the first username on the page to determine the nominator.

const matches = content.match(/\[\[User:(.*?)[\]|]/);

if (nomType('nominations') && matches) {

allNominators = [matches[1]];

data = `${allNominators.length} ${pluralize(

nomTypeText,

allNominators.length

)}`;

// This is not a nomination-type, and we couldn't find any relevant

// users, so we have to assume that there are none.

} else {

data = `0 ${pluralize(nomTypeText, 0)}`;

}

}

addNewNomData({

pageName,

data,

id: 'nominators',

hoverText: `${pluralize(

capitalize(nomTypeText),

allNominators.length

)} (sorted alphabetically): ${allNominators.join(' ')}`,

});

}

/**

* Generate the patterns used to find vote text.

*

* @returns {Object} The patterns.

*/

function getVoteTextAndPatterns() {

// Look for text that is enclosed within bold text, or level-4 (or greater)

// headings.

const wrapPattern = "('''|====)";

// The amount of characters allowed between the vote text, and the wrapping

// patterns.

const voteBuffer = 25;

const textPattern = `(.{0,${voteBuffer}})?`;

let openPattern = `${wrapPattern}${textPattern}`;

let closePattern = `${textPattern}${wrapPattern}`;

let supportText = 'support';

let opposeText = 'oppose';

// Use different words for review pages.

if (nomType('reviews')) {

supportText = 'keep';

opposeText = 'delist';

// Pictures has their own specific method of declaring votes.

} else if (nomType('pictures')) {

openPattern = "\\*(\\s)?'''.*?";

closePattern = ".*?'''";

}

const createPattern = (text) =>

new RegExp(

`(${openPattern}${text}${closePattern}|^;${textPattern}${text})`,

'gim'

);

return {

supportText,

supportPattern: createPattern(supportText),

opposeText,

opposePattern: createPattern(opposeText),

};

}

function shouldShowVotes() {

const showOpposesForNominations = false;

const showOpposesForReviews = true;

return (

((nomType('nominations') || nomType('pictures')) &&

showOpposesForNominations) ||

(nomType('reviews') && showOpposesForReviews)

);

}

/**

* Add votes data to a nomination.

*

* @param {string} content The nomination's content.

* @param {string} pageName The page name.

* @returns {undefined}

*/

function addVotes(content, pageName) {

if (!dataIsEnabled('votes') || nomType('peer reviews')) {

return;

}

const {

supportText,

supportPattern,

opposeText,

opposePattern,

} = getVoteTextAndPatterns();

const supportMatches = content.match(supportPattern) || [];

const opposeMatches = content.match(opposePattern) || [];

const supports = `${supportMatches.length} ${pluralize(

supportText,

supportMatches.length

)}`;

const opposes = `, ${opposeMatches.length} ${pluralize(

opposeText,

opposeMatches.length

)}`;

addNewNomData({

pageName,

data: shouldShowVotes() ? supports + opposes : supports,

id: 'votes',

hoverText: supports + opposes,

});

}

function currentRevisionCallback(object) {

const vars = formatJSON(object);

if (!vars) {

return;

}

const content = vars.firstRevision ? vars.firstRevision['*'] : null;

if (!content) {

return;

}

// 'images'

addImagesCount(content, vars.pageName);

// 'nominators'

addNominators(content, vars.pageName);

// 'votes'

addVotes(content, vars.pageName);

}

function addAge(firstRevision, pageName) {

if (!dataIsEnabled('age') || !firstRevision) {

return;

}

const { timeAgo, then } = getTimeAgo(firstRevision.timestamp);

addNewNomData({

pageName,

data: timeAgo,

id: 'age',

hoverText: `Creation date (local time): ${then}`,

});

}

function addLastEdit(lastRevision, pageName) {

if (!dataIsEnabled('lastedit') || !lastRevision) {

return;

}

const { timeAgo, then } = getActivity(lastRevision.timestamp);

addNewNomData({

pageName,

data: timeAgo,

id: 'lastedit',

hoverText: `Last edit date (local time): ${then}`,

});

}

// Callback helpers

function capitalize(string) {

return string.charAt(0).toUpperCase() + string.slice(1);

}

/**

* Check if the data field is enabled.

*

* @param {string} dataName The name of the data field to look up.

* @returns {boolean} The data field is enabled, so we want to use it.

*/

function dataIsEnabled(dataName) {

return NominationsViewer.nominationData.some((data) => dataName === data);

}

// Given `content`, find nominators with the `pattern`. Returns an Object, so

// that we exclude duplicates.

function findNominators(content, pattern) {

const nominatorMatches = content.match(pattern);

const listOfNominators = {};

if (!nominatorMatches) {

return listOfNominators;

}

// Find nominator usernames.

// Example, Wikipedia talk:WikiProject Example

let nominators = nominatorMatches[0].match(

/\[\[(user|wikipedia|wp|wt)([ _]talk)?:.*?]]/gi

);

if (nominators) {

nominators.forEach((nominator) => {

// Strip unneeded characters from the nominator's URL.

let username = nominator

// Strip the start of the username link.

.replace(/\[\[(user|wikipedia|wp|wt)([ _]talk)?:/i, '')

// Strip the displayed portion of the username link.

.replace(/\|.*/, '')

// Strip the ending portion of the username link.

.replace(']]', '')

// Strip URL anchors.

.replace(/#.*?$/, '');

// Does 'username' have a '/' that we have to strip?

if (username.includes('/')) {

username = username.slice(0, Math.max(0, username.indexOf('/')));

}

listOfNominators[username] += 1;

});

}

// {{user|Example}} and similar variants

const userTemplatePattern = /{{user.*?\|(.*?)}}/gi;

nominators = nominatorMatches[0].match(userTemplatePattern);

if (nominators) {

nominators.forEach((singleNominator) => {

listOfNominators[

singleNominator.replace(userTemplatePattern, '$1')

] += 1;

});

}

return listOfNominators;

}

function formatJSON(object) {

if (!object.query || !object.query.pages) {

return false;

}

const vars = [];

vars.pages = object.query.pages;

vars.page = Object.keys(vars.pages).map((page) => page);

if (vars.page.length !== 1) {

return false;

}

vars.page = object.query.pages[vars.page[0]];

vars.pageName = vars.page.title.replace(/\s/g, '_');

if (!vars.page.revisions) {

return false;

}

[vars.firstRevision] = vars.page.revisions;

[vars.lastRevision] = vars.page.revisions.slice(-1);

vars.revisions = vars.page.revisions;

return vars;

}

/**

* Check if the nomination type of the current nomination is the type

* specified. If no type is specified, then return the type of the current

* nomination. Possible types are: `nominations`, `peer reviews`, `pictures`,

* and `reviews`, as specified in `NominationsViewer.enabledPages`.

*

* @param {string} [type] The type to compare the current nomination with.

* @returns {boolean|string} The current nomination matches the type

* specified, or the type of the current nomination.

*/

function nomType(type = null) {

const pageName = mw.config.get('wgPageName').replace(/_/g, ' ');

const pageType = NominationsViewer.enabledPages[pageName];

if (type) {

return type === pageType;

}

return pageType;

}

/**

* Pluralize a word if necessary.

*

* @param {string} string The word to possibly pluralize.

* @param {number} count The number of items there are.

* @returns {string} The pluralized word.

*/

function pluralize(string, count) {

const plural = `${string}s`;

if (count === 1) {

return string;

}

return plural;

}

/**

* Format a page name by remove any non-word characters.

*

* @param {string} pageName The page name to format.

* @returns {string} The formatted page name.

*/

function simplifyPageName(pageName) {

return pageName.replace(/\W/g, '');

}

/**

* Given a timestamp, generally calculate the time ago.

*

* @param {string} timestamp A timestamp.

* @returns {Object.} The time ago phrase.

*/

function getTimeAgo(timestamp) {

const matches = timestamp.match(

/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z/

);

const now = new Date();

const then = new Date(

Date.UTC(

matches[1],

matches[2] - 1,

matches[3],

matches[4],

matches[5],

matches[6]

)

);

const millisecondsAgo = now.getTime() - then.getTime();

const daysAgo = Math.floor(millisecondsAgo / (1000 * 60 * 60 * 24));

let timeAgo = '';

if (daysAgo > 0) {

const weeksAgo = Math.round(daysAgo / 7);

const monthsAgo = Math.round(daysAgo / 30);

const yearsAgo = Math.round(daysAgo / 365);

if (yearsAgo >= 1) {

timeAgo = `${yearsAgo} ${pluralize('year', yearsAgo)} old`;

} else if (monthsAgo >= 3) {

timeAgo = `${monthsAgo} ${pluralize('month', monthsAgo)} old`;

} else if (weeksAgo >= 1) {

timeAgo = `${weeksAgo} ${pluralize('week', weeksAgo)} old`;

} else {

timeAgo = `${daysAgo} ${pluralize('day', daysAgo)} old`;

}

} else {

timeAgo = 'today';

}

return { timeAgo, then };

}

function getActivity(timestamp) {

const matches = timestamp.match(

/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z/

);

const now = new Date();

const then = new Date(

Date.UTC(

matches[1],

matches[2] - 1,

matches[3],

matches[4],

matches[5],

matches[6]

)

);

const millisecondsAgo = now.getTime() - then.getTime();

const daysAgo = Math.floor(millisecondsAgo / (1000 * 60 * 60 * 24));

let timeAgo = '';

if (daysAgo > 0) {

const weeksAgo = Math.round(daysAgo / 7);

const monthsAgo = Math.round(daysAgo / 30);

const yearsAgo = Math.round(daysAgo / 365);

if (yearsAgo >= 1) {

timeAgo = `Inactive for ${yearsAgo} ${pluralize('year', yearsAgo)}`;

} else if (monthsAgo >= 3) {

timeAgo = `Inactive for ${monthsAgo} ${pluralize('month', monthsAgo)}`;

} else if (weeksAgo >= 1) {

timeAgo = `Inactive for ${weeksAgo} ${pluralize('week', weeksAgo)}`;

} else {

timeAgo = `Active ${daysAgo} ${pluralize('day', daysAgo)} ago`;

}

} else {

timeAgo = 'Active today';

}

return { timeAgo, then };

}

init();

});

//