User:Polygnotus/Scripts/RFCTracker.js
/**
* RfC Tracker - shows up to 5 newly-opened RfCs in the sidebar.
* "New" means: the parent revision did not yet carry the RfC tag.
* Uses the Page Visibility API to refresh when the tab becomes visible.
* Requires: [[User:Polygnotus/Helpers/Sidebar.js]]
* Install: paste into [[Special:MyPage/common.js]]
*/
( function () {
'use strict';
// Refuse to run without the Page Visibility API (also used below for refresh)
if ( typeof document.hidden === 'undefined' ) {
return;
}
var API_ENDPOINT = '/w/api.php';
var helper;
/* ------------------------------------------------------------------ */
/* Fetch */
/* ------------------------------------------------------------------ */
// Generation counter — incremented before every fetch.
// Each XHR callback closes over its own generation value and bails out
// if a newer fetch has since started, preventing stale results from
// overwriting fresher ones after rapid tab switching.
var fetchGeneration = 0;
function fetchRfCs() {
var generation = ++fetchGeneration;
helper.setHeadingLabel( 'Recent RfCs (…)' );
// Edits and new pages: an RfC can be placed on a freshly-created talk
// page (type=new) or added to an existing page (type=edit).
// Fetch 50 so that filtering still leaves us with 5.
var params = new URLSearchParams( {
action: 'query',
list: 'recentchanges',
rctag: 'RfC',
rclimit: '50',
rcprop: 'title|timestamp|user|ids',
rctype: 'edit|new',
rcshow: '!bot',
format: 'json',
origin: '*'
} );
var xhr = new XMLHttpRequest();
xhr.open( 'GET', API_ENDPOINT + '?' + params.toString(), true );
xhr.setRequestHeader( 'Api-User-Agent', 'RfCTrackerUserscript/1.0 (common.js gadget)' );
xhr.onreadystatechange = function () {
if ( xhr.readyState !== 4 ) { return; }
if ( generation !== fetchGeneration ) { return; } // stale response — discard
if ( xhr.status !== 200 ) {
helper.setHeadingLabel( 'Recent RfCs (error)' );
return;
}
try {
var data = JSON.parse( xhr.responseText );
var changes = data.query && data.query.recentchanges;
if ( !changes ) { throw new Error( 'Bad response' ); }
filterNewRfCs( changes, generation );
} catch ( e ) {
helper.setHeadingLabel( 'Recent RfCs (error)' );
}
};
xhr.send( null );
}
/**
* Sort a copy of changes newest-first, then remove duplicate page titles,
* keeping the first (most recent) occurrence.
* Sorting internally makes this function self-contained and not dependent
* on the caller providing pre-sorted input.
*/
function deduplicateByTitle( changes ) {
var sorted = changes.slice().sort( function ( a, b ) {
return new Date( b.timestamp ) - new Date( a.timestamp );
} );
var seen = {};
return sorted.filter( function ( rc ) {
if ( seen[ rc.title ] ) { return false; }
seen[ rc.title ] = true;
return true;
} );
}
/**
* Keep entries where the RfC tag was introduced by this revision.
* - type === 'new': page was just created (e.g. a new talk page) with an
* RfC already on it, so the RfC is always new by definition.
* - type === 'edit': check the parent revision; skip if it already carried
* the RfC tag (meaning the RfC predates this edit).
* After filtering, truncate to 5 results.
*/
function filterNewRfCs( changes, generation ) {
var newPages = changes.filter( function ( rc ) { return rc.type === 'new'; } );
var edits = changes.filter( function ( rc ) { return rc.type === 'edit'; } );
if ( edits.length === 0 ) {
helper.markDataLoaded();
renderRows( deduplicateByTitle( newPages ).slice( 0, 5 ) );
return;
}
// Slice edits to 49 before deriving oldRevIds so that introducingEdits
// is filtered against the same subset. Without this, edits beyond
// index 48 would have parent revids never sent to the API, causing them
// to pass the filter incorrectly. MediaWiki's revids limit is 50;
// staying at 49 leaves room for edge cases.
var editsToCheck = edits.slice( 0, 49 );
var oldRevIds = editsToCheck
.map( function ( rc ) { return rc.old_revid; } )
.filter( Boolean );
if ( oldRevIds.length === 0 ) {
helper.markDataLoaded();
renderRows( deduplicateByTitle( newPages ).slice( 0, 5 ) );
return;
}
var params = new URLSearchParams( {
action: 'query',
prop: 'revisions',
revids: oldRevIds.join( '|' ),
rvprop: 'ids|tags',
format: 'json',
origin: '*'
} );
var xhr = new XMLHttpRequest();
xhr.open( 'GET', API_ENDPOINT + '?' + params.toString(), true );
xhr.setRequestHeader( 'Api-User-Agent', 'RfCTrackerUserscript/1.0 (common.js gadget)' );
xhr.onreadystatechange = function () {
if ( xhr.readyState !== 4 ) { return; }
if ( generation !== fetchGeneration ) { return; } // stale response — discard
if ( xhr.status !== 200 ) {
helper.setHeadingLabel( 'Recent RfCs (error)' );
return;
}
try {
var data = JSON.parse( xhr.responseText );
var pages = data.query && data.query.pages;
if ( !pages ) { throw new Error( 'Bad response' ); }
// Build a set of parent revids that already carried the RfC tag
var parentHadRfC = {};
Object.keys( pages ).forEach( function ( pageId ) {
var revs = pages[ pageId ].revisions || [];
revs.forEach( function ( rev ) {
if ( rev.tags && rev.tags.indexOf( 'RfC' ) !== -1 ) {
parentHadRfC[ rev.revid ] = true;
}
} );
} );
// Keep only edits (from the checked subset) whose parent did
// NOT have the RfC tag
var introducingEdits = editsToCheck.filter( function ( rc ) {
return !parentHadRfC[ rc.old_revid ];
} );
var combined = newPages.concat( introducingEdits );
combined.sort( function ( a, b ) {
return new Date( b.timestamp ) - new Date( a.timestamp );
} );
helper.markDataLoaded();
renderRows( deduplicateByTitle( combined ).slice( 0, 5 ) );
} catch ( e ) {
helper.setHeadingLabel( 'Recent RfCs (error)' );
}
};
xhr.send( null );
}
/* ------------------------------------------------------------------ */
/* Render */
/* ------------------------------------------------------------------ */
function renderRows( changes ) {
var ul = document.createElement( 'ul' );
ul.className = 'vector-menu-content-list';
if ( changes.length === 0 ) {
var empty = document.createElement( 'li' );
empty.className = 'mw-list-item';
var emptySpan = document.createElement( 'span' );
emptySpan.textContent = 'No results.';
empty.appendChild( emptySpan );
ul.appendChild( empty );
} else {
changes.forEach( function ( rc ) {
var li = document.createElement( 'li' );
li.className = 'mw-list-item';
var articleLink = document.createElement( 'a' );
// mw.util.getUrl handles all title encoding correctly,
// including titles that contain literal '%' characters which
// encodeURIComponent() would double-encode.
articleLink.href = mw.util.getUrl( rc.title );
var articleSpan = document.createElement( 'span' );
articleSpan.textContent = rc.title;
articleLink.appendChild( articleSpan );
li.appendChild( articleLink );
var diffLink = document.createElement( 'a' );
diffLink.href = '/w/index.php?diff=' + rc.revid;
diffLink.style.marginLeft = '4px';
var diffSpan = document.createElement( 'span' );
diffSpan.textContent = '(diff)';
diffLink.appendChild( diffSpan );
li.appendChild( diffLink );
var meta = document.createElement( 'div' );
meta.style.fontSize = 'x-small';
meta.style.color = '#555';
meta.textContent = new Date( rc.timestamp ).toLocaleString() + ' · ' + rc.user;
li.appendChild( meta );
ul.appendChild( li );
} );
}
helper.replaceRows( ul );
helper.setHeadingLabel( 'Recent RfCs' );
}
/* ------------------------------------------------------------------ */
/* Page Visibility API - refresh when tab becomes visible */
/* ------------------------------------------------------------------ */
// Registered inside $(function(){}) so the widget shell is guaranteed to
// exist in the DOM before the first visibilitychange event can fire and
// call helper.setHeadingLabel(). Attaching it here (outside DOM ready)
// would cause setHeadingLabel() to silently no-op if the event fired
// before the shell was inserted.
function attachVisibilityListener() {
document.addEventListener( 'visibilitychange', function () {
if ( !document.hidden ) {
fetchRfCs();
// Incrementing fetchGeneration inside fetchRfCs() automatically
// cancels any in-flight request pair from a previous visibility
// event, so no additional debouncing is needed here.
}
} );
}
/* ------------------------------------------------------------------ */
/* Init */
/* ------------------------------------------------------------------ */
mw.loader.using( 'mediawiki.util' ).then( function () {
mw.loader.getScript(
'https://en.wikipedia.org/w/index.php?title=User:Polygnotus/Helpers/Sidebar.js&action=raw&ctype=text/javascript'
).then( function () {
helper = window.SidebarHelper( {
id : 'p-rfc-tracker',
storageKey : 'rfcTrackerCollapsed',
heading : 'Recent RfCs',
btnClass : 'rfc-collapse-btn',
onExpand : fetchRfCs
} );
$( function () {
// Render the shell so the widget exists in the DOM before
// fetchRfCs() calls helper.setHeadingLabel('…') or the
// error handler calls it with 'error'.
renderRows( [] );
if ( !helper.isCollapsed() ) {
fetchRfCs();
}
// Attach after the shell is in the DOM so setHeadingLabel()
// always finds the widget when a visibilitychange event fires.
attachVisibilityListener();
} );
}, function () {
mw.log.error( 'RfC Tracker: failed to load SidebarHelper.' );
} );
} );
}() );
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.