![]() |
VOOZH | about |
Note: After saving, you have to bypass your browser's cache to see the changes.
Google Chrome, Firefox, Microsoft Edge, and Safari: Hold down the key and click the Reload toolbar button.
For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* Remaining issues: * Log edits except move (protect, import, revert) show as unpatrolled, despite not being real edits that can be patrolled, as there is no way to reliably detect them (they just look like normal edits, other than having a particular summary, which anyone could fake in any edit) * Imported edits show as unpatrolled, as there is no way to tell they are imported * Rollbacked edits show as unpatrolled if the rollback isn't visible */ // TODO: Revert to less ugly version once MW allows ES6+ // jshint jquery:true, esversion:5 /* globals require, module, mediaWiki, mw, OO */ 'use strict'; // TODO: Use async once MW allows it $(function(){ vari18n={ autopatrolled:'Autopatrolled', // $1 is the user, $2 is the timestamp patrolled:'Patrolled by $1 ($2)', unpatrollable:'Unpatrollable', unpatrolled:'Unpatrolled', autopatrolledLegend:'autopatrolled edit', patrolledLegend:'patrolled edit', unpatrollableLegend:'unpatrollable edit', unpatrolledLegend:'unpatrolled edit', error:'Failed to retrieve patrol information', rightsError:'Failed to retrieve autopatrol rights', errorTitle:'RevisionPatrol', }; // Only run on real pages if(mw.config.get('wgNamespaceNumber')===-1){ return; } // Script only works on history and diff pages varmode=mw.config.get('wgAction'); if(mode==='view'&&document.querySelector('.diff')){ mode='diff'; } if(mode!=='history'&&mode!=='diff'){ return; } // Try to format a date in the user's interface language and timezone, // falling back to UTC, then falling back to just a plain timestamp varformatDate=(function(){ varlang=mw.config.get('wgUserLanguage'); // MW uses normal English date format, despite defaulting to US English if(lang==='en'){ lang='en-GB'; } // This will only work if the user set a timezone name, not a manual offset. // Dunno how to give Intl an offset, or convert an offset to a timezone name vartimezone=mw.user.options.get('timecorrection').split('|')[2]; if(!timezone){ timezone='UTC'; } // Doesn't seem to be a simple way to test if Intl is supported, as older // browsers support just some of the features (timezones), so just have a go // and fallback to the timestamp if it doesn't work vardateFormatter; try{ dateFormatter=newIntl.DateTimeFormat(lang,{ day:'numeric',month:'long',year:'numeric', hour:'numeric',minute:'numeric',hour12:false, timeZone:timezone, }); }catch(e){} returnfunction(date){ returndateFormatter?dateFormatter.format(newDate(date)):date; }; }()); vararrayChunk=function(inArr,chunkSize){ varoutArr=[]; for(vari=0;i<inArr.length;i+=chunkSize){ outArr.push(inArr.slice(i,i+chunkSize)); } returnoutArr; }; // Gets the page name from a full URL // Would've thought MW would have a built-in function for this varpageFromUrl=function(url){ returnmw.util.getParamValue('title',url)||decodeURIComponent( url.slice(( mw.config.get('wgServer')+ mw.config.get('wgArticlePath').slice(0,-2) ).length) ); }; // Find the original page name of a moved revision // Returns false if it wasn't moved, null if it was moved but the page // was unable to be determined, or the page name if it was moved varfindMovedRev=function(revElem){ if(!revElem.querySelector('.mw-tag-marker-move')){ returnfalse; } varpageLink=revElem.querySelector('.comment > a'); // Page name was too long to include in the summary, RIP if(!pageLink){ returnnull; } returnpageFromUrl(pageLink.href); }; varmakePatrolIcon=function(status,user,timestamp){ varelem=document.createElement('span'); elem.className='revisionpatrol-icon-'+status; if(status==='patrolled'){ elem.title=i18n[status] .replace(/\$1/g,user) .replace(/\$2/g,formatDate(timestamp)); }else{ elem.title=i18n[status]; } returnelem; }; varapi=newmw.Api(); varpageName=mw.config.get('wgPageName').replace(/_/g,' '); // Get the ids for all the revisions on the page, bucketed by page name at // the time, and store their associated element varrevBuckets={}; varusers={}; varnewestRevId; varoldestRevId; varcheckMove=true; varrollback; if(mode==='history'){ varcurPage=pageName; varbucket={}; varcurId; varrevElems=document.querySelectorAll('#pagehistory li'); // TODO: Use for..of once MW allows it Array.prototype.forEach.call(revElems,function(li,i){ varprevPage=findMovedRev(li); // Start a new bucket for moved revisions if(prevPage!==false){ if(i===0){ checkMove=false; }else{ if(curPage!==null){ revBuckets[curPage]=bucket; } bucket=revBuckets[prevPage]||{}; } curPage=prevPage; return; } if(curPage===null){ return; } varrevUrl=li.querySelector('.mw-changeslist-date').href; curId=Number(mw.util.getParamValue('oldid',revUrl)); if(!newestRevId){ newestRevId=curId; } bucket[curId]=li; bucket.end=curId; varuser=li.querySelector('.mw-userlink'); if(user){ users[user.textContent]=true; } if(li.querySelector('.mw-tag-marker-mw-rollback')){ rollback=true; }elseif(rollback){ if(rollback===true||rollback===user){ rollback=user; li.classList.add('revisionpatrol-autopatrolled'); }else{ rollback=false; } } }); revBuckets[curPage]=bucket; oldestRevId=curId; }else{ varnewestRev=document.querySelector('.diff-ntitle'); varnewestPage=findMovedRev(newestRev); varrollback; if(newestPage===false){ newestRevId=mw.config.get('wgRevisionId'); revBuckets[pageName]={}; revBuckets[pageName][newestRevId]=newestRev; revBuckets[pageName].end=newestRevId; varuser=newestRev.querySelector('.mw-userlink'); if(user){ users[user.textContent]=true; } if(newestRev.querySelector('.mw-tag-marker-mw-rollback')){ rollback=true; } }else{ checkMove=false; } varoldestRev=document.querySelector('.diff-otitle'); varpage=newestPage||pageName; if(findMovedRev(oldestRev)===false){ varoldestRevHead=document.querySelector('#mw-diff-otitle1 > strong'); varoldestRevUrl=oldestRevHead.querySelector('a').href; oldestRevId=Number(mw.util.getParamValue('oldid',oldestRevUrl)); revBuckets[page]=(revBuckets[page]||{}); revBuckets[page][oldestRevId]=oldestRev; revBuckets[page].end=oldestRevId; varuser=oldestRev.querySelector('.mw-userlink'); if(user){ users[user.textContent]=true; } if(rollback&&!document.querySelector('.diff-multi')){ oldestRevHead.classList.add('revisionpatrol-autopatrolled'); } } oldestRevId=oldestRevId||newestRevId; newestRevId=newestRevId||oldestRevId; } // Nothing patrollable on the page if(!oldestRevId){ return; } // No need to check if the page was moved if we can see the latest revision if(newestRevId===mw.config.get('wgCurRevisionId')){ checkMove=false; } // Get userrights for users on the page, to check if they have autopatrol // in chunks of 50. This is done in parallel to the next request as rights // seems to be slow to request (and need not delay finding patrolled edits), // and to simplify the request, due to how the API handles continuing varusersChunks=arrayChunk(Object.keys(users),50); varmakeRightsReq=function(){ returnapi.post({ action:'query', list:'users', ususers:usersChunks.shift(), usprop:'rights', formatversion:2, }).then(function(resp){ varapUsers={}; resp.query.users.forEach(function(user){ if(user.rights&&user.rights.indexOf('autopatrol')>-1){ apUsers[user.name]=true; } }); if(usersChunks.length){ returnmakeRightsReq().then(function(data){ returnObject.assign(apUsers,data); }); } returnapUsers; },function(code,data){ console.error(code,data.error&&data.error.info); setTimeout(function(){ mw.notify( i18n.rightsError, {title:i18n.errorTitle,type:'error',autoHide:false} ); },2000); }); }; varrightsReq=makeRightsReq(); // Get: // * oldest recentchanges entry to work out what the patrollable age limit is // * ids and timestamps for the revisions on the page // * if we need to check if the page was moved, also get the other revisions // til the current one, with their tags and parsedcomment. // TODO: Use await once MW allows it varrcQuery={ list:'recentchanges', rcprop:'timestamp', rcdir:'newer', rclimit:1, }; varrevQuery={ titles:pageName, prop:'revisions', rvprop:['ids','timestamp'], rvstartid:oldestRevId, rvdir:'newer', rvlimit:'max', }; if(checkMove){ revQuery.rvprop.push('tags','parsedcomment'); }else{ revQuery.rvendid=newestRevId; } varmakeRevsRcReq=function(cont){ varquery={ action:'query', formatversion:2, }; if(!cont){ Object.assign(query,rcQuery); } if(!cont||cont.rvcontinue){ Object.assign(query,revQuery); } Object.assign(query,cont); varrevsRcReq=api.get(query) .fail(function(code,data){ console.error(code,data.error&&data.error.info); }); returnrevsRcReq.then(function(resp){ varlimit; varrc=resp.query.recentchanges; if(rc){ limit=newDate(rc[0].timestamp).getTime(); // Don't continue RC request, we only wanted the first result deleteresp['continue'].rccontinue; } vardates={}; varhaveAllDates; varfoundMove=!checkMove; resp.query.pages[0].revisions.some(function(rev){ dates[rev.revid]=rev.timestamp; if(!foundMove&&haveAllDates&&rev.tags.indexOf('move')>-1){ foundMove=true; // Update page name varcomment=$('<i>').html(rev.parsedcomment)[0]; varcurPageLink=comment.querySelector('a'); varcurPage; if(curPageLink){ curPage=pageFromUrl(curPageLink.href); revBuckets[curPage]=revBuckets[pageName]; } // If the page was originally the title it is now, then we // can just keep the current bucket name if(curPage!==pageName){ // Delete the old page name regardless of if we found the // current one as we can't check the patrols without it deleterevBuckets[pageName]; } } if(rev.revid===newestRevId){ haveAllDates=true; } returnhaveAllDates&&foundMove; }); cont=resp['continue']; if(cont&&cont.rvcontinue&&(!haveAllDates||!foundMove)){ returnmakeRevsRcReq(cont).then(function(data){ return{limit:limit,dates:Object.assign(dates,data.dates)}; }); } return{limit:limit,dates:dates}; }); }; makeRevsRcReq().then(function(data){ vardates=data.dates; varpatrolLimit=data.limit; varpatrolReqs=[rightsReq]; $.each(revBuckets,function(page,ids){ varendId=ids.end; if(!endId){ return; } deleteids.end; vartotalRevs=Object.keys(ids).length; // Get all the patrols that happened after the last revision varpatrolQuery={ action:'query', list:'logevents', letitle:page, leaction:'patrol/patrol', leprop:['details','user','timestamp'], lestart:dates[endId], ledir:'newer', lelimit:'max', formatversion:2, }; varmakePatrolReq=function(cont){ // TODO: Use await once MW allows it varpatrolReq=api.get(Object.assign({},patrolQuery,cont)) .fail(function(code,data){ console.error(code,data.error&&data.error.info); }); returnpatrolReq.then(function(resp){ // Mark off patrolled edits as we go, as older history pages may // take multiple requests before all the patrol entries are checked // TODO: Use for..of once MW allows it resp.query.logevents.forEach(function(patrol){ varrevElem=ids[patrol.params.curid]; if(!revElem){ return; } varstatusElem=mode==='diff'? revElem.querySelector('div > strong'):revElem; // Autopatrols prior to MW 1.27 are marked as normal patrols but // with the auto param set if(patrol.params.auto){ statusElem.classList.add('revisionpatrol-autopatrolled'); --totalRevs; return; } statusElem.classList.add('revisionpatrol-patrolled'); varicon=makePatrolIcon('patrolled',patrol.user,patrol.timestamp); statusElem.insertBefore(icon,statusElem.firstChild); // Remove any revisions previously assumed to be autopatrolled statusElem.classList.remove('revisionpatrol-autopatrolled'); deleteids[patrol.params.curid]; --totalRevs; }); cont=resp['continue']; if(cont&&totalRevs>0){ returnmakePatrolReq(cont); } }); }; patrolReqs.push(makePatrolReq().then(function(){ // Anything not marked as patrolled still, is autopatrolled or not patrolled rightsReq.then(function(apUsers){ // TODO: Use for..of once MW allows it Object.keys(ids).forEach(function(revId){ varrevElem=ids[revId]; varstatusElem=mode==='diff'? revElem.querySelector('div > strong'):revElem; varstatus; varuser=revElem.querySelector('.mw-userlink'); if( statusElem.classList.contains('revisionpatrol-autopatrolled')|| (user&&apUsers[user.textContent]) ){ status='autopatrolled'; }else{ status=newDate(dates[revId]).getTime()<patrolLimit? 'unpatrollable':'unpatrolled'; } statusElem.classList.add('revisionpatrol-'+status); varicon=makePatrolIcon(status); statusElem.insertBefore(icon,statusElem.firstChild); }); }); },function(){ // Mission failed, we'll get 'em next time setTimeout(function(){ mw.notify( i18n.error, {title:i18n.errorTitle,type:'error',autoHide:false} ); },2000); return; })); }); if(mode!=='history'){ return; } $.when.apply(null,patrolReqs).then(function(){ // Add the patrol icons to the legend varlegendElem=document.querySelector('.mw-history-legend > p'); varaddLegend=function(status){ if(!document.querySelector('.revisionpatrol-'+status)){ return; } legendElem.innerHTML+=', <span class="revisionpatrol-icon-'+ status+'-legend"> = '+i18n[status+'Legend']+'</span>'; }; legendElem.innerHTML=legendElem.innerHTML.trim().slice(0,-1); addLegend('patrolled'); addLegend('autopatrolled'); addLegend('unpatrolled'); addLegend('unpatrollable'); legendElem.innerHTML+='.'; }); },function(){ // Not worth looking through all patrols just for the one page // If this request failed, the next one probably would too anyway setTimeout(function(){ mw.notify( i18n.error, {title:i18n.errorTitle,type:'error',autoHide:false} ); },2000); return; }); // Update diff when page is patrolled if(mode==='diff'){ $('.patrollink').find('a').click(function(){ // The page is considered patrolled once the patrollink is gone varcheckPatrolStatus=setInterval(function(){ if(document.querySelector('.patrollink')){ return; } clearInterval(checkPatrolStatus); varstatusElem=document.querySelector('#mw-diff-ntitle1 > strong'); statusElem.classList.remove('revisionpatrol-unpatrolled'); statusElem.classList.add('revisionpatrol-patrolled'); varicon=makePatrolIcon('patrolled',mw.config.get('wgUserName'),Date.now()); statusElem.querySelector('.revisionpatrol-icon-unpatrolled').remove(); statusElem.insertBefore(icon,statusElem.firstChild); },500); }); } });