The official discord link if you wish to join the discord: https://discord.gg/j5RKwCvAFu

Support the wiki on our official Ko-Fi page or Patreon page!

MediaWiki:Timeless.js

From The Codex

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* All JavaScript here will be loaded for users of the Timeless skin */
if (typeof rcwidget !== 'undefined') {
    // Clean up existing instance
    if (rcwidget.cleanup) {
        rcwidget.cleanup();
    }
}
 
var rcwidget = (function() {
    // Private variables
    let fetchInterval = null;
    let updateInterval = null;
 
    // Configuration
    const config = {
        onlyshowores: (typeof onlyshowores !== 'undefined') ? onlyshowores : false,
        orestolerance: (typeof orestolerance !== 'undefined') ? orestolerance : 0.70,
        fetchtime: (typeof rcfetchtime !== 'undefined') ? rcfetchtime : 0.5
    };
 
    // Utility functions
    function generateTimeAgo(hours, minutes, seconds) {
        if (hours > 0) {
            return `${hours}${hours === 1 ? ' hour' : ' hours'} ago`;
        } else if (minutes > 0) {
            return `${minutes}${minutes === 1 ? ' minute' : ' minutes'} ago`;
        } else if (seconds > 0) {
            return `${seconds}${seconds === 1 ? ' second' : ' seconds'} ago`;
        }
        return 'just now';
    }
 
    function createChangeHTML(change) {
        try {
            const baseUrl = mw.config.get("wgScript");
            let html = '<li>';
 
            // Handle ORES scoring
            const shouldBold = change.type === "edit" && 
                             change.oresscores?.damaging?.true >= config.orestolerance;
 
            if (shouldBold) {
                html += '<b>';
            }
 
            // User link
            html += `<a href="${baseUrl}/User_talk:${encodeURIComponent(change.user)}">${change.user}</a> `;
 
            // Change type specific content
            if (change.type === "edit") {
                if (change.tags.includes("mw-undo")) {
                    html += `<a href="${baseUrl}/Project:Undo">undid</a> an `;
                } else if (change.tags.includes("mw-rollback")) {
                    html += `<a href="${baseUrl}/Project:Rollback">rolled back</a> `;
                }
 
                html += `<a href="${baseUrl}/Special:Diff/${change.revid}">edit</a> to `;
            }
 
            // Page link
            if (change.ns === 3) {
                html += `left a message for <a href="${baseUrl}/${encodeURIComponent(change.title)}">${change.title.replace("User talk:", "")}</a>`;
            } else if (change.type === "new") {
                html += `created the page <a href="${baseUrl}/${encodeURIComponent(change.title)}">${change.title}</a>`;
            } else {
                html += `<a href="${baseUrl}/${encodeURIComponent(change.title)}">${change.title}</a>`;
            }
 
            if (shouldBold) {
                html += '</b>';
            }
 
            // Timestamp
            const changeDate = new Date(change.timestamp);
            const currDate = new Date();
            const mildate = new Date(currDate - changeDate);
 
            html += `<br><small class="rcwidget-date" data-revtimestamp="${change.timestamp}">` +
                   generateTimeAgo(mildate.getUTCHours(), mildate.getUTCMinutes(), mildate.getUTCSeconds()) +
                   '</small></li>';
 
            return html;
        } catch (error) {
            console.error('Error generating change HTML:', error);
            return '';
        }
    }
 
    // Main widget functions
    function addToSidebar(text) {
        try {
            const skin = mw.config.get("skin");
 
            // Clean up any existing widgets
            $('.rcwidget-instance').remove();
 
            const widget = $('<div>')
                .addClass('rcwidget-instance')
                .html(text);
 
            if (skin === "timeless") {
                widget.addClass("sidebar-chunk")
                    .appendTo("#mw-related-navigation");
                $("#catlinks-sidebar, #other-languages").appendTo("#mw-related-navigation");
            } else if (skin === "vector" || skin === "vector-2022") {
                widget.addClass("mw-portlet mw-portlet-navigation vector-menu vector-menu-portal")
                    .prependTo("#mw-panel");
            } else {
                widget.addClass("portlet")
                    .prependTo("#mw_portlets");
            }
 
            // Handle navigation elements
            $("#p-navigation").prependTo("#mw-panel");
            $("#p-search").prependTo("#quickbar");
            $('#p-logo').prependTo("#mw-site-navigation, #mw-panel, #sidebar, #mw_portlets");
            $('ul.hlist:first').appendTo('#mw-mf-page-left');
        } catch (error) {
            console.error('Error adding sidebar:', error);
        }
    }
 
    function fetch() {
        if (!document.hasFocus()) {
            return;
        }
 
        $.get(mw.config.get("wgScriptPath") + "/api.php", {
            action: "query",
            format: "json",
            list: "recentchanges",
            rcnamespace: "0|3",
            rcprop: "title|timestamp|flags|loginfo|oresscores|parsedcomment|user|ids|tags",
            rcshow: "!bot" + (config.onlyshowores ? "|oresreview" : ""),
            rctoponly: true,
            rclimit: "50",
            rctype: "edit|new"
        })
        .done(function(result) {
            try {
                if (result.error) {
                    throw new Error(result.error.info);
                }
 
                if (!result.query?.recentchanges) {
                    throw new Error('Invalid API response structure');
                }
 
                const changes = result.query.recentchanges
                    .filter(change => {
                        if (!config.onlyshowores) return true;
                        return change.oresscores?.damaging?.true >= config.orestolerance;
                    })
                    .map(createChangeHTML)
                    .filter(html => html);
 
                let html = '<ul>' + changes.join('');
 
                // Add "View all" link
                html += `<li><a href="${mw.config.get("wgScript")}/Special:RecentChanges">View all recent changes</a></li></ul>`;
 
                const $content = $("#rcwidget-content");
                $content.html(changes.length ? html : "<ul><li>No recent changes found.</li></ul>");
            } catch (error) {
                console.error('Error processing recent changes:', error);
                $("#rcwidget-content").html("<ul><li>Error loading recent changes.</li></ul>");
            }
        })
        .fail(function(jqXHR, textStatus, errorThrown) {
            console.error('API request failed:', textStatus, errorThrown);
            $("#rcwidget-content").html("<ul><li>Error loading recent changes.</li></ul>");
        });
    }
 
    function updateTimestamps() {
        $('.rcwidget-date').each(function() {
            try {
                const $this = $(this);
                const changeDate = new Date($this.data('revtimestamp'));
                const currDate = new Date();
                const mildate = new Date(currDate - changeDate);
 
                $this.html(generateTimeAgo(
                    mildate.getUTCHours(),
                    mildate.getUTCMinutes(),
                    mildate.getUTCSeconds()
                ));
            } catch (error) {
                console.error('Error updating timestamp:', error);
            }
        });
    }
 
    function init() {
        if (!window.jQuery || !window.mw) {
            console.error('Required dependencies (jQuery or MediaWiki) not found');
            return;
        }
 
        try {
            const baseUrl = mw.config.get("wgScript");
            const skin = mw.config.get('skin');
 
            const widgetContent = (skin === 'vector' || skin === 'vector-2022') ?
                `<div id="rcwidget-label" lang="en" dir="ltr">
                    <span><a href="${baseUrl}/Special:RecentChanges">Recent changes</a></span>
                </div>
                <div class="mw-portlet-body body pBody" id="rcwidget-content" style="height:250px;overflow:hidden;">
                    Loading...
                </div>` :
                `<h3 id="rcwidget-label" lang="en" dir="ltr">
                    <span><a href="${baseUrl}/Special:RecentChanges">Recent changes</a></span>
                </h3>
                <div class="mw-portlet-body body pBody" id="rcwidget-content" style="height:250px;overflow:hidden;">
                    Loading...
                </div>`;
 
            addToSidebar(widgetContent);
 
            // Set up intervals
            fetch();
            fetchInterval = window.setInterval(fetch, config.fetchtime * 1000);
            updateInterval = window.setInterval(updateTimestamps, config.fetchtime * 1000);
        } catch (error) {
            console.error('Error initializing widget:', error);
        }
    }
 
    function cleanup() {
        try {
            if (fetchInterval) {
                clearInterval(fetchInterval);
            }
            if (updateInterval) {
                clearInterval(updateInterval);
            }
            $('.rcwidget-instance').remove();
        } catch (error) {
            console.error('Error cleaning up widget:', error);
        }
    }
 
    // Public API
    return {
        init: init,
        cleanup: cleanup,
        fetch: fetch
    };
})();
 
// Initialize widget when ready
$(document).ready(rcwidget.init);