Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// Generated by ChatGPT o4-mini and GPT-4o

// Prompts below:

// Write me JS code to display recent changes for a wiki using MediaWiki Action API
// Make sure it is ES6-compatible so it can work on MediaWiki:Common.js on a MediaWiki wiki.
/*
[[User:Xeverything11|Xeverything11]] ([[User talk:Xeverything11|talk]])
<templatestyles src="RecentChanges/styles.css" />
<div class="mw-parser-output">
<div class="rc-container">
<div class="rc-header">
<div class="rc-unpatrolled">[[Special:RecentChanges|<span id="RCUnpatrolled">16</span> {{material icon|1=<span id="RCUnpatrolledIcon">sunny</span>|size=inherit}}]]</div>
Changes to patrol</div>
<div class="rc-main" id="RCMain">
JavaScript is required to view the recent changes.
</div>
</div>
</div>
[[User:Xeverything11|Xeverything11]] ([[User talk:Xeverything11|talk]])

1. Remove all children from #RCMain
2. Prepend three most recent changes into #RCMain.
3. Wait 3 seconds.
4. Prepend the next recent change (4th).
5. Repeat steps 3-4. (5th, 6th, 7th change etc.)

Format:
<username> <action> <page>
<time ago> ago

Example
Xeverything11 edited Make Vegan Gingerbread
13 hours ago
*/
// 1ms after each RC prepends, add .rc-active CSS class to that (to allow transitions).
/*
edit -> edited
new -> created
log seems vague, it can be "uploaded", "moved", "blocked", "protected" etc.
*/
/*
For move:
Xeverything11 moved User:Xeverything11/Use Google Analytics -> Xeverything11 moved User:Xeverything11/Use Google Analytics to Use Google Analytics
 
<username> moved <source page> -> <username> moved <source page> to <username> moved <source page> to <target page>
*/
/*
#RCUnpatrolled: shows number of unpatrolled changes
#RCUnpatrolledIcon: shows text (0 to 9 unpatrolled changes = sunny, 10 to 19 = partly_cloudy_day, 20 to 34 = cloud, 35 to 49 = rainy, 50+ = thunderstorm)
*/
// I mean "Add this after setting up the API (const api = new mw.Api();):"
// Combine into one module
// It didn't work. What's wrong?

mw.loader.using('mediawiki.api').then(() => {
  // wait until the browser has built the DOM
  if ( document.readyState === 'loading' ) {
    document.addEventListener('DOMContentLoaded', initRecentChanges);
  } else {
    initRecentChanges();
  }
});

function initRecentChanges() {
  const api = new mw.Api();
  const RC_CONTAINER     = document.getElementById('RCMain');
  const UNPATROLLED_EL   = document.getElementById('RCUnpatrolled');
  const ICON_EL          = document.getElementById('RCUnpatrolledIcon');
  if ( !RC_CONTAINER ) {
    console.error('Recent-changes container (#RCMain) not found');
    return;
  }

  // Format "X minutes/hours ago"
  const timeAgo = timestamp => {
    const seconds = Math.floor((Date.now() - new Date(timestamp)) / 1000);
    const intervals = [
      { label: 'day', secs: 86400 },
      { label: 'hour', secs: 3600 },
      { label: 'minute', secs: 60 },
      { label: 'second', secs: 1 }
    ];
    for (const { label, secs } of intervals) {
      const count = Math.floor(seconds / secs);
      if (count > 0) {
        return `${count} ${label}${count > 1 ? 's' : ''}`;
      }
    }
    return 'just now';
  };

  // Create formatted action line for a change
  const formatActionLine = change => {
    const user = mw.html.escape(change.user);
    const source = mw.html.escape(change.title);
    const link = `${mw.config.get('wgServer') + mw.config.get('wgScriptPath')}/index.php?title=${encodeURIComponent(change.title)}`;

    if (change.type === 'edit') {
      return `${user} edited <a href="${link}">${source}</a>`;
    }

    if (change.type === 'new') {
      return `${user} created <a href="${link}">${source}</a>`;
    }

    if (change.type === 'log') {
      if (change.logtype === 'move' && change.loginfo && change.loginfo.target_title) {
        const target = mw.html.escape(change.loginfo.target_title);
        const targetLink = `${mw.config.get('wgServer') + mw.config.get('wgScriptPath')}/index.php?title=${encodeURIComponent(change.loginfo.target_title)}`;
        return `${user} moved <a href="${link}">${source}</a> to <a href="${targetLink}">${target}</a>`;
      }

      const logVerbMap = {
        upload: 'uploaded',
        block: 'blocked',
        protect: 'protected',
        delete: 'deleted',
        rights: 'changed rights',
        patrol: 'patrolled',
        import: 'imported',
        tag: 'tagged'
      };

      const verb = logVerbMap[change.logtype] || 'performed log action';
      return `${user} ${verb} <a href="${link}">${source}</a>`;
    }

    return `${user} ${change.type} <a href="${link}">${source}</a>`;
  };

  // Create and insert one change item
  const prependChange = change => {
    const entry = document.createElement('div');
    entry.className = 'rc-entry';
    entry.innerHTML = `
      <div class="rc-line1">${formatActionLine(change)}</div>
      <div class="rc-line2">${timeAgo(change.timestamp)} ago</div>
    `;
    RC_CONTAINER.insertBefore(entry, RC_CONTAINER.firstChild);
    setTimeout(() => entry.classList.add('rc-active'), 50);
  };

  // Load recent changes
  const loadRecentChanges = () => {
    api.get({
      action: 'query',
      list: 'recentchanges',
      rcprop: 'title|ids|sizes|comment|user|timestamp|flags|loginfo',
      rclimit: 100,
      rcshow: '!bot',
      format: 'json'
    }).then(data => {
      const changes = data.query.recentchanges;
      RC_CONTAINER.replaceChildren();
      changes.slice(0, 3).forEach(prependChange);
      let i = 3;
      const timer = setInterval(() => {
        if (i >= changes.length) {
          clearInterval(timer);
        } else {
          prependChange(changes[i++]);
        }
      }, 3000);
    }).catch(err => {
      console.error('Failed to load recent changes:', err);
      RC_CONTAINER.textContent = 'Error loading recent changes.';
    });
  };

  // Update unpatrolled count and icon
  const updateUnpatrolledStatus = () => {
    api.get({
      action: 'query',
      list: 'recentchanges',
      rcprop: 'ids',
      rcshow: 'unpatrolled',
      rclimit: 'max',
      format: 'json'
    }).then(data => {
      const count = data.query.recentchanges.length;
      if (UNPATROLLED_EL) UNPATROLLED_EL.textContent = count;

      if (ICON_EL) {
        let icon = 'sunny';
        if (count >= 50) icon = 'thunderstorm';
        else if (count >= 35) icon = 'rainy';
        else if (count >= 20) icon = 'cloud';
        else if (count >= 10) icon = 'partly_cloudy_day';
        ICON_EL.textContent = icon;
      }
    }).catch(err => {
      console.warn('Could not fetch unpatrolled count:', err);
    });
  };

  // Run both features
  loadRecentChanges();
  updateUnpatrolledStatus();
};