User:PresN/nominations viewer.js

// // Nominations Viewer // // Description: Compact nominations for WP:FAC, WP:FAR, WP:FLC, //  WP:FLRC, WP:FPC, and WP:PR. // Documentation: 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&#10;Total edits: ${          revisions.length        }&#10;Format: &quot;editor: \  number of edits&quot;:&#10;&#10;${usersArray2.join('&#10;')}`,      }); }   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):&#10;&#10;${images.join( '&#10;'       )}`,      });    }    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):&#10;&#10;${allNominators.join('&#10;')}`,     }); }   /**     * 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):&#10;&#10;${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):&#10;&#10;${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;        }); }     //  and similar variants const userTemplatePattern = //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; }); //