MediaWiki:Gadget-ChangestoPatrol.js: Difference between revisions
Page last edited 3 months ago by Xeverything11
test RC icon |
m Xeverything11 moved page MediaWiki:Common.js to MediaWiki:Gadget-ChangestoPatrol.js without leaving a redirect: try again |
||
(9 intermediate revisions by the same user not shown) | |||
Line 46: | Line 46: | ||
<username> moved <source page> -> <username> moved <source page> to <username> moved <source page> to <target page> | <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(() => { | 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 api = new mw.Api(); | ||
const RC_CONTAINER = document.getElementById('RCMain'); | 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 timeAgo = timestamp => { | ||
const seconds = Math.floor((Date.now() - new Date(timestamp)) / 1000); | const seconds = Math.floor((Date.now() - new Date(timestamp)) / 1000); | ||
Line 68: | Line 91: | ||
}; | }; | ||
// | // Create formatted action line for a change | ||
const formatActionLine = change => { | const formatActionLine = change => { | ||
const user = mw.html.escape(change.user); | const user = mw.html.escape(change.user); | ||
Line 107: | Line 130: | ||
}; | }; | ||
// | // Create and insert one change item | ||
const prependChange = change => { | const prependChange = change => { | ||
const entry = document.createElement('div'); | const entry = document.createElement('div'); | ||
entry.className = 'rc-entry'; | entry.className = 'rc-entry'; | ||
entry.innerHTML = ` | entry.innerHTML = ` | ||
<div class="rc-line1"> | <div class="rc-line1">${formatActionLine(change)}</div> | ||
<div class="rc-line2">${timeAgo(change.timestamp)} ago</div> | |||
<div class="rc-line2"> | |||
`; | `; | ||
RC_CONTAINER.insertBefore(entry, RC_CONTAINER.firstChild); | RC_CONTAINER.insertBefore(entry, RC_CONTAINER.firstChild); | ||
setTimeout(() => | 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(); | |||
}; | }; |
Latest revision as of 20:41, 24 June 2025
// 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();
};