User:Enterprisey/section-watchlist.js
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 ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// vim: ts=4 sw=4 et
$.when( mw.loader.using( [ "mediawiki.api" ] ), $.ready ).then( function () {
var api = new mw.Api();
var PARSOID_ENDPOINT = "https:" + mw.config.get( "wgServer" ) + "/api/rest_v1/page/html/";
var HEADER_REGEX = /^\s*=(=*)\s*(.+?)\s*\1=\s*$/gm;
var BACKEND_URL = "https://section-watchlist.toolforge.org";
var TOKEN_OPTION_NAME = "userjs-section-watchlist-token";
var LOCAL_STORAGE_PREFIX = "wikipedia-section-watchlist-";
var LOCAL_STORAGE_PAGE_LIST_KEY = LOCAL_STORAGE_PREFIX + "page-list";
var LOCAL_STORAGE_EXPIRY_KEY = LOCAL_STORAGE_PREFIX + "expiry";
var PAGE_LIST_EXPIRY_MILLIS = 7 * 24 * 60 * 60 * 1000; // a week
var ENTERPRISEY_ENWP_TALK_PAGE_LINK = '<a href="https://en.wikipedia.org/wiki/User talk:Enterprisey/section-watchlist" title="User talk:Enterprisey/section-watchlist on the English Wikipedia">User talk:Enterprisey/section-watchlist</a>';
var CORS_ERROR_MESSAGE = 'Error contacting the server. It might be down, in which case ' + ENTERPRISEY_ENWP_TALK_PAGE_LINK + ' (en.wiki) will have updates.';
/////////////////////////////////////////////////////////////////
//
// Utilities
// Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes
if( !String.prototype.includes ) {
String.prototype.includes = function( search, start ) {
if( search instanceof RegExp ) {
throw TypeError('first argument must not be a RegExp');
}
if( start === undefined ) {
start = 0;
}
return this.indexOf( search, start ) !== -1;
};
}
// Polyfill from https://github.com/jonathantneal/array-flat-polyfill, which is CC0-licensed
if( !Array.prototype.flat ) {
Object.defineProperty( Array.prototype, 'flat', {
configurable: true,
value: function flat () {
var depth = isNaN( arguments[0] ) ? 1 : Number( arguments[0] );
return depth ? Array.prototype.reduce.call( this, function ( acc, cur ) {
if( Array.isArray( cur ) ) {
acc.push.apply( acc, flat.call( cur, depth - 1 ) );
} else {
acc.push( cur );
}
return acc;
}, [] ) : Array.prototype.slice.call( this );
},
writable: true
} );
}
// https://stackoverflow.com/a/9229821/1757964
function removeDuplicates( array ) {
var seen = {};
return array.filter( function( item ) {
return seen.hasOwnProperty( item ) ? false : ( seen[ item ] = true );
} );
}
function lastInArray( array ) {
return array[ array.length - 1 ];
}
function pageNameOfHeader( header ) {
var editLinks = Array.prototype.slice.call( header.querySelectorAll( "a" ) )
.filter( function ( e ) { return e.textContent.indexOf( "edit" ) === 0; } );
if( editLinks.length ) {
var encoded = editLinks[0]
.getAttribute( "href" )
.match( /title=(.+?)(?:$|&)/ )
[1];
return decodeURIComponent( encoded ).replace( /_/g, " " );
} else {
return null;
}
}
var getAllTranscludedTitlesCache = null;
function getAllTranscludedTitles() {
if( !getAllTranscludedTitlesCache ) {
var allHeadersArray = Array.prototype.slice.call(
document.querySelector( "#mw-content-text" ).querySelectorAll( "h1,h2,h3,h4,h5,h6" ) );
getAllTranscludedTitlesCache = removeDuplicates( allHeadersArray
.filter( function ( header ) {
// The word "Contents" at the top of the table of contents is a heading
return header.getAttribute( "id" ) !== "mw-toc-heading"
} )
.map( pageNameOfHeader )
.filter( Boolean ) );
}
return getAllTranscludedTitlesCache;
}
/////////////////////////////////////////////////////////////////
//
// User interface for normal pages
function loadPagesWatched() {
try {
var expiryStr = window.localStorage.getItem(LOCAL_STORAGE_EXPIRY_KEY);
if( expiryStr ) {
var expiry = parseInt( expiryStr );
if( expiry && ( ( new Date().getTime() - expiry ) < PAGE_LIST_EXPIRY_MILLIS ) ) {
var list = window.localStorage.getItem(LOCAL_STORAGE_PAGE_LIST_KEY);
return $.when( { status: "success", data: list.split( "," ) } );
}
}
var url = BACKEND_URL + "/subbed_pages?user_id=" +
mw.config.get( "wgUserId" ) + "&token=" + mw.user.options.get( TOKEN_OPTION_NAME );
return $.getJSON( url ).then( function ( data ) {
if( data.status === "success" ) {
try {
window.localStorage.setItem(LOCAL_STORAGE_EXPIRY_KEY, new Date().getTime());
window.localStorage.setItem(LOCAL_STORAGE_PAGE_LIST_KEY, data.data.join( "," ));
} catch ( e ) {
console.error( e );
}
}
return data;
} );
} catch ( e ) {
console.error( e );
}
}
function loadSectionsWatched( allTranscludedIds ) {
var promises = allTranscludedIds.map( function ( id ) {
return $.getJSON( BACKEND_URL + "/subbed_sections?page_id=" +
id + "&user_id=" +
mw.config.get( "wgUserId" ) + "&token=" + mw.user.options.get( TOKEN_OPTION_NAME ) );
} );
return $.when.apply( $, promises ).then( function () {
var obj = {};
if( allTranscludedIds.length === 1 ) {
if( arguments[0].status === "success" ) {
obj[allTranscludedIds[0]] = arguments[0].data;
return { status: "success", data: obj };
} else {
return arguments[0];
}
} else {
var groupStatus = "";
var errorMessage = null;
for( var i = 0; i < arguments.length; i++ ) {
if( arguments[i][0].status !== "success" ) {
allSuccess = false;
errorMessage = arguments[i][0].data;
} else {
obj[allTranscludedIds[i]] = arguments[i][0].data;
}
if( groupStatus === "success" ) {
groupStatus = arguments[i][0].status;
}
}
return {
status: groupStatus,
data: ( groupStatus === "success" ) ? obj : errorMessage
};
}
} );
}
function initializeFakeLinks( messageHtml ) {
mw.loader.using( [ "mediawiki.util", "oojs-ui-core", "oojs-ui-widgets" ] );
$( "#mw-content-text" ).find( "h1,h2,h3,h4,h5,h6" ).each( function ( idx, header ) {
var popup = null;
$( header ).find( ".mw-editsection *" ).last().before(
"<span style='color: #54595d'> | </span>",
$( "<span>" ).append(
$( "<a>" )
.attr( "href", "#" )
.text( "watch" )
.click( function () {
if( popup === null ) {
mw.loader.using( [ "mediawiki.util", "oojs-ui-core", "oojs-ui-widgets" ] ).then( function () {
popup = new OO.ui.PopupWidget( {
$content: $( '<p>', { style: 'padding-top: 0.5em' } ).html( messageHtml ),
padded: true,
width: 400,
align: 'forwards',
hideCloseButton: false,
} );
$( this ).parent().append( popup.$element );
popup.toggle( true );
}.bind( this ) );
} else {
popup.toggle();
}
return false;
} ) ) );
} );
}
function attachLink( header, pageId, pageName, wikitextName, dupIdx, isAlreadyWatched ) {
$( header ).find( ".mw-editsection *" ).last().before(
"<span style='color: #54595d'> | </span>",
$( "<a>" )
.attr( "href", "#" )
.text( isAlreadyWatched ? "unwatch" : "watch" )
.click( function () {
var link = $( this );
if( !mw.user.options.get( TOKEN_OPTION_NAME ) ) {
alert( "You must register first by visiting Special:BlankPage/section-watchlist." );
return false;
}
var data = {
page_id: pageId,
page_title: pageName,
section_name: wikitextName,
section_dup_idx: dupIdx,
user_id: mw.config.get( "wgUserId" ),
token: mw.user.options.get( TOKEN_OPTION_NAME )
};
if( this.textContent === "watch" ) {
$.post( BACKEND_URL + "/sub", data ).then( function ( data2 ) {
if( data2.status === "success" ) {
link.text( "unwatch" );
try {
var list = window.localStorage.getItem( LOCAL_STORAGE_PAGE_LIST_KEY ) || "";
if( !list.includes( pageId ) ) {
window.localStorage.setItem( LOCAL_STORAGE_PAGE_LIST_KEY, list + "," + pageId );
}
} catch ( e ) {
console.error( e );
}
} else {
console.error( data2 );
}
}, function ( request ) {
if( request.responseJSON && request.responseJSON.status ) {
console.error( request.responseJSON );
}
console.error( request );
} );
} else {
$.post( BACKEND_URL + "/unsub", data ).then( function ( data2 ) {
if( data2.status === "success" ) {
link.text( "watch" );
} else {
console.error( data2 );
}
}, function ( request ) {
if( request.responseJSON && request.responseJSON.status ) {
console.error( request.responseJSON );
}
console.error( request );
} );
}
return false;
} ) );
}
function initializeLinks( transcludedTitlesAndIds, allWatchedSections ) {
var allHeadersArray = Array.prototype.slice.call(
document.querySelector( "#mw-content-text" ).querySelectorAll( "h1,h2,h3,h4,h5,h6" ) );
var allHeaders = allHeadersArray
.filter( function ( header ) {
// The word "Contents" at the top of the table of contents is a heading
return header.getAttribute( "id" ) !== "mw-toc-heading"
} )
.map( function ( header ) {
return [ header, pageNameOfHeader( header ) ];
} )
.filter( function ( headerAndPage ) {
return headerAndPage[1] !== null
} );
var allTranscludedTitles = removeDuplicates( allHeaders.map( function ( header ) { return header[1]; } ) );
return api.get( {
action: "query",
prop: "revisions",
titles: allTranscludedTitles.join("|"),
rvprop: "content",
rvslots: "main",
formatversion: 2
} ).then( function( revData ) {
for( var pageIdx = 0; pageIdx < revData.query.pages.length; pageIdx++ ) {
var targetTitle = revData.query.pages[pageIdx].title;
var targetPageId = revData.query.pages[pageIdx].pageid;
var targetWikitext = revData.query.pages[pageIdx].revisions[0].slots.main.content;
var watchedSections = allWatchedSections ? allWatchedSections[targetPageId] : {};
var allHeadersFromTarget = allHeaders.filter( function ( header ) { return header[1] === targetTitle; } );
// Find all the headers in the wikitext
// (Nowiki exclusion code copied straight from reply-link)
// Save all nowiki spans
var nowikiSpanStarts = []; // list of ignored span beginnings
var nowikiSpanLengths = []; // list of ignored span lengths
var NOWIKI_RE = /<(nowiki|pre)>[\s\S]*?<\/\1>/g;
var spanMatch;
do {
spanMatch = NOWIKI_RE.exec( targetWikitext );
if( spanMatch ) {
nowikiSpanStarts.push( spanMatch.index );
nowikiSpanLengths.push( spanMatch[0].length );
}
} while( spanMatch );
// So that we don't check every ignore span every time
var nowikiSpanStartIdx = 0;
var headerMatches = [];
var headerMatch;
matchLoop:
do {
headerMatch = HEADER_REGEX.exec( targetWikitext );
if( headerMatch ) {
// Check that we're not inside a nowiki
for( var nwIdx = nowikiSpanStartIdx; nwIdx <
nowikiSpanStarts.length; nwIdx++ ) {
if( headerMatch.index > nowikiSpanStarts[nwIdx] ) {
if ( headerMatch.index + headerMatch[0].length <=
nowikiSpanStarts[nwIdx] + nowikiSpanLengths[nwIdx] ) {
// Invalid sig
continue matchLoop;
} else {
// We'll never encounter this span again, since
// headers only get later and later in the wikitext
nowikiSpanStartIdx = nwIdx;
}
}
}
headerMatches.push( headerMatch );
}
} while( headerMatch );
// We'll use this dictionary to calculate the duplicate index
var headersByText = {};
for( var i = 0; i < headerMatches.length; i++ ) {
// Group 2 of HEADER_REGEX is the header text
var text = headerMatches[i][2];
headersByText[text] = ( headersByText[text] || [] ).concat( i );
}
// allHeadersFromTarget should contain every header we found in the wikitext
// (and more, if targetPageName was transcluded multiple times)
if( allHeadersFromTarget.length % headerMatches.length !== 0 ) {
console.error(allHeadersFromTarget);
console.error(headerMatches);
throw new Error( "non-divisble header list lengths" );
}
for( var headerIdx = 0; headerIdx < allHeadersFromTarget.length; headerIdx++ ) {
var trueHeaderIdx = headerIdx % headerMatches.length;
var headerText = headerMatches[trueHeaderIdx][2];
// NOTE! The duplicate index is calculated relative to the
// *wikitext* header matches (because that's how the backend
// does it)! That is, if we have a page that includes two
// headers, both called "a", and we transclude that page
// twice, the result will be four headers called "a". But we
// want to assign those four headers, respectively, the
// duplicate indices of 0, 1, 0, 1. That's why we use
// trueHeaderIdx here, not headerIdx.
var dupIdx = headersByText[headerText].indexOf( trueHeaderIdx );
var headerEl = allHeadersFromTarget[headerIdx];
var headerId = headerEl[0].querySelector( "span.mw-headline" ).id;
var isAlreadyWatched = ( watchedSections[headerText] || [] ).indexOf( dupIdx ) >= 0;
attachLink( headerEl, targetPageId, targetTitle, headerText, dupIdx, isAlreadyWatched );
}
}
}, function () {
console.error( arguments );
} );
}
/////////////////////////////////////////////////////////////////
//
// The watchlist page
function parseSimpleAddition( diffHtml ) {
var CONTEXT_ROW = /<tr>\n <td class="diff-marker"> <\/td>\n <td class="diff-context">(?:<div>([^<]*)<\/div>)?<\/td>\n <td class="diff-marker"> <\/td>\n <td class="diff-context">.*?<\/td>\n<\/tr>\n/g;
var ADDED_ROW = /<tr>\n <td colspan="2" class="diff-empty"> <\/td>\n <td class="diff-marker">\+<\/td>\n <td class="diff-addedline">(?:<div>([^<]*)<\/div>)?<\/td>\n<\/tr>\n/g;
function consecutiveMatches( regex, text ) {
var prevMatchEndIdx = null;
var match = null;
var rows = [];
while( ( match = regex.exec( text ) ) !== null ) {
if( ( prevMatchEndIdx !== null ) && ( prevMatchEndIdx !== match.index ) ) {
// this match wasn't immediately after the previous one
break;
}
rows.push( match[1] || "" );
prevMatchEndIdx = match.index + match[0].length;
}
return {
text: rows.join( "\n" ),
endIdx: prevMatchEndIdx
};
}
var prevContext = consecutiveMatches( CONTEXT_ROW, diffHtml );
var added = consecutiveMatches( ADDED_ROW, diffHtml.substring( prevContext.endIdx ) );
function fix( text ) {
var INS_DEL = /<ins class="diffchange diffchange-inline">|<\/ins>|<del class="diffchange diffchange-inline">|<\/del>/g;
var ENTITIES = /&(lt|gt|amp);/g;
return text.replace( INS_DEL, "" ).replace( ENTITIES, function ( _match, group1 ) {
switch( group1 ) {
case "lt": return "<";
case "gt": return ">";
case "amp": return "&";
}
} );
}
return {
prevContext: fix( prevContext.text ),
added: fix( added.text )
};
}
function handleViewNewText( listElement, streamEvent, sectionEvent ) {
api.get( {
action: "compare",
fromrev: streamEvent.data.revision["new"],
torelative: "prev",
formatversion: "2",
prop: "diff"
} ).then( function ( compareResponse ) {
var diffHtml = compareResponse.compare.body;
var parsedDiff = parseSimpleAddition( diffHtml );
var addedHtmlPromise = $.post( {
url: "https:" + mw.config.get( "wgServer" ) + "/w/api.php",
data: {
action: "parse",
format: "json",
formatversion: "2",
title: streamEvent.title,
text: parsedDiff.added,
prop: "text", // just wikitext, please
pst: "1" // do the pre-save transform
}
} );
var listElementAddedPromise = addedHtmlPromise.then( function ( newHtmlResponse ) {
listElement.append( newHtmlResponse.parse.text );
var newContent = listElement.find( ".mw-parser-output" );
mw.hook( "wikipage.content" ).fire( $( newContent ) );
} );
var revObjPromise = api.get( {
action: "query",
prop: "revisions",
rvprop: "timestamp|content|ids",
rvslots: "main",
rvlimit: 1,
titles: streamEvent.title,
formatversion: 2,
} ).then( function ( data ) {
if( data.query.pages[0].revisions ) {
var rev = data.query.pages[0].revisions[0];
return { revId: rev.revid, timestamp: rev.timestamp, content: rev.slots.main.content };
} else {
console.error( data );
throw new Error( "[getWikitext] bad response: " + data );
}
} );
$.when(
addedHtmlPromise,
revObjPromise,
listElementAddedPromise
).then( function ( newHtmlResponse, revObj, _ ) {
// Walmart reply-link
var namespace = streamEvent.namespace;
var ttdykPage = streamEvent.title.indexOf( "Template:Did_you_know_nominations" ) === 0;
if( ( namespace % 2 ) === 1 || namespace === 4 || ttdykPage ) {
// Ideally this is kept in sync with the one defined
// near the top of reply-link; if they differ, I imagine
// the reply-link one is correct
var REPLY_LINK_TIMESTAMP_REGEX = /\(UTC(?:(?:−|\+)\d+?(?:\.\d+)?)?\)\S*?\s*$/m;
var newContent = listElement.find( ".mw-parser-output" ).get( 0 );
if( REPLY_LINK_TIMESTAMP_REGEX.test( newContent.textContent ) ) {
var nodeToAttachAfter = newContent.children[0];
do {
nodeToAttachAfter = lastInArray( nodeToAttachAfter.childNodes );
} while( lastInArray( nodeToAttachAfter.childNodes ).nodeType !== 3 /* Text */ );
nodeToAttachAfter = lastInArray( nodeToAttachAfter.childNodes );
var parentCmtIndentation = /^[:*#]*/.exec( parsedDiff.added )[0];
var sectionName = sectionEvent.target[0].replace( /_/g, " " );
var headerRegex = new RegExp( "^=(=*)\\s*" + mw.util.escapeRegExp( sectionName ) + "\\s*\\1=\\s*$", "gm" );
var sectionDupIdx = sectionEvent.target[1];
for( var i = 0; i < sectionDupIdx; i++ ) {
// Advance the regex past all the previous duplicate matches
headerRegex.exec( revObj.content );
}
var headerMatch = headerRegex.exec( revObj.content );
var REPLY_LINK_HEADER_REGEX = /^\s*=(=*)\s*(.+?)\s*\1=\s*$/gm;
var endOfThatHeaderIdx = headerMatch.index + headerMatch[0].length;
var nextHeaderMatch = REPLY_LINK_HEADER_REGEX.exec( revObj.content.substring( endOfThatHeaderIdx ) );
var nextHeaderIdx = endOfThatHeaderIdx + ( nextHeaderMatch ? nextHeaderMatch.index : revObj.content.length );
var parentCmtEndStrIdx = revObj.content.indexOf( parsedDiff.prevContext ) +
parsedDiff.prevContext.length + parsedDiff.added.length - headerMatch.index;
mw.hook( "replylink.attachlinkafter" ).fire(
nodeToAttachAfter,
/* preferredId */ "",
/* parentCmtObj */ {
indentation: parentCmtIndentation,
sigIdx: null,
endStrIdx: parentCmtEndStrIdx
},
/* sectionObj */ {
title: sectionName,
dupIdx: sectionDupIdx,
startIdx: headerMatch.index,
endIdx: nextHeaderIdx,
idxInDomHeaders: null,
pageTitle: streamEvent.title.replace( /_/g, " " ),
revObj: revObj,
headerEl: null
}
);
} else {
console.warn( "text content didn't match timestamp regex" );
}
} else {
console.warn( "bad namespace " + namespace );
}
} );
} );
}
function renderLengthDiff( beforeLength, afterLength ) {
var delta = afterLength - beforeLength;
var el = ( Math.abs( delta ) > 500 ) ? "strong" : "span";
var elClass = "mw-plusminus-" + ( ( delta > 0 ) ? "pos" : ( ( delta < 0 ) ? "neg" : "null" ) );
return $( "<span>", { "class": "mw-changeslist-line-inner-characterDiff" } ).append(
$( "<" + el + ">", {
"class": elClass + " mw-diff-bytes",
"dir": "ltr",
"title": afterLength + " byte" + ( ( afterLength === 1 ) ? "" : "s" ) + " after change of this size"
} ).text( ( ( delta > 0 ) ? "+" : "" ) + mw.language.convertNumber( delta ) ) );
}
function renderItem( streamEvent, sectionEvent ) {
var url = mw.util.getUrl( streamEvent.title ) + "#" + sectionEvent.target[0];
var els = [
streamEvent.timestamp.substring( 8, 10 ) + ":" + streamEvent.timestamp.substring( 10, 12 ),
$( "<span>", { "class": "mw-changeslist-line-inner-articleLink" } ).append(
$( "<span>", { "class": "mw-title" } ).append(
$( "<a>", { "class": "mw-changeslist-title", "href": url, "title": streamEvent.title } )
.text( streamEvent.title + " § " + sectionEvent.target[0].replace( /_/g, " " ) ) ) ),
// TODO pending support for "vague sections"
//sectionEvent.target[2]
// ? $( "<span>" ).append( "(under ", $( "<a>", { "href": secondaryUrl } ).text( streamEvent.target[2][0] ) )
// : "",
streamEvent.data.revision["new"]
? $( "<span>", { "class": "mw-changeslist-line-inner-historyLink" } ).append(
$( "<span>", { "class": "mw-changeslist-links" } ).append(
$( "<span>" ).append(
// The URL parameters must be in this order, or Navigation Popups will not work for this link. (UGH.)
$( "<a>", {
"class": "mw-changeslist-diff",
"href": mw.util.getUrl( "", {
"title": streamEvent.title,
"diff": "prev",
"oldid": streamEvent.data.revision["new"]
} )
} ).text( "diff" ) ),
$( "<span>" ).append(
$( "<a>", { "class": "mw-changeslist-history", "href": mw.util.getUrl( streamEvent.title, { "action": "history" } ) } )
.text( "hist" ) ),
) )
: "",
$( "<span>", { "class": "mw-changeslist-line-inner-separatorAfterLinks" } ).append(
$( "<span>", { "class": "mw-changeslist-separator" } ) ),
renderLengthDiff( streamEvent.data.length.old, streamEvent.data.length["new"] ),
$( "<span>", { "class": "mw-changeslist-line-inner-separatorAftercharacterDiff" } ).append(
$( "<span>", { "class": "mw-changeslist-separator" } ) ),
$( "<span>", { "class": "mw-changeslist-line-inner-userLink" } ).append(
$( "<a>", { "class": "mw-userlink", "href": mw.util.getUrl( "User:" + streamEvent.user ), "title": "User:" + streamEvent.user } ).append(
$( "<bdi>" ).text( streamEvent.user ) ) ),
$( "<span>", { "class": "mw-changeslist-line-inner-userTalkLink" } ).append(
$( "<span>", { "class": "mw-usertoollinks mw-changeslist-links" } ).append(
$( "<span>" ).append(
$( "<a>", { "class": "mw-usertoollinks-talk", "href": mw.util.getUrl( "User talk:" + streamEvent.user ), "title": "User talk:" + streamEvent.user } )
.text( "talk" ) ),
$( "<span>" ).append(
$( "<a>", { "class": "mw-usertoollinks-contribs", "href": mw.util.getUrl( "Special:Contributions/" + streamEvent.user ), "title": "Special:Contributions/" + streamEvent.user } )
.text( "contribs" ) ) ) ),
streamEvent.data.minor
? $( "<abbr>", { "class": "minoredit", "title": "This is a minor edit" } ).text( "m" )
: "",
$( "<span>", { "class": "mw-changeslist-line-inner-comment" } ).append(
$( "<span>", { "class": "comment comment--without-parentheses" } ).append(
$( "<span>", { "dir": "auto" } ).append( streamEvent.parsedcomment ) ) )
];
if( streamEvent.data.is_simple_addition ) {
els.push( $( "<span>" ).append( "(", $( "<a>", { "class": "section-watchlist-view-new-text", "href": "#" } ).text( "view new text" ), ")" ) );
}
for( var i = els.length - 1; i >= 0; i-- ) {
els.splice( i, 0, " " );
}
return els;
}
function renderInbox( inbox ) {
var days = [];
var currDateString; // for example, the string "20200701", meaning "1 July 2020"
var currItems = []; // the inbox entries for the current day, sorted from latest to earliest
for( var i = 0; i < inbox.length; i++ ) {
var streamEventAndSectionEvent = inbox[i];
var streamEvent = streamEventAndSectionEvent.stream;
var sectionEvent = streamEventAndSectionEvent.section;
if( streamEvent.timestamp.substring( 0, 8 ) !== currDateString ) {
if( currItems.length ) {
days.push( [ currDateString, currItems ] );
}
currItems = [];
currDateString = streamEvent.timestamp.substring( 0, 8 );
}
if( sectionEvent.type === "Edit" ) {
var sectionName = sectionEvent.target[0];
var listEl = $( "<li>" ).append( renderItem( streamEvent, sectionEvent ) );
if( streamEvent.data.is_simple_addition ) {
( function () {
var currStreamEvent = streamEvent;
var currSectionEvent = sectionEvent;
listEl.find( ".section-watchlist-view-new-text" ).click( function ( evt ) {
var parserOutput = this.parentNode.parentNode.querySelector( ".mw-parser-output" );
if( parserOutput ) {
$( parserOutput ).toggle();
} else {
handleViewNewText( $( this ).parent().parent(), currStreamEvent, currSectionEvent );
}
if( this.textContent === "view new text" ) {
this.textContent = "hide new text";
} else {
this.textContent = "view new text";
}
evt.preventDefault();
return false;
} );
} )();
}
currItems.push( listEl );
} else {
currItems.push( $( "<li>" ).text( JSON.stringify( streamEvent ) + " | " + JSON.stringify( sectionEvent ) ) );
}
}
if( currItems.length ) {
days.push( [ currDateString, currItems ] );
}
return days;
}
// "20200701" -> "July 1" (in the user's interface language... approximately)
// TODO there really has to be a better way to do this
var englishMonths = [
'january', 'february', 'march', 'april',
'may', 'june', 'july', 'august',
'september', 'october', 'november', 'december'
];
function renderIsoDate( isoDate ) {
return mw.msg( englishMonths[ parseInt( isoDate.substring( 4, 6 ) ) - 1 ] ) + " " + parseInt( isoDate.substring( 6, 8 ) );
}
// i.e. generate a message in the case that we have no token.
function generateNoTokenMessage( registerUrl ) {
return $.ajax( {
type: "HEAD",
"async": true,
url: BACKEND_URL
} ).then( function () {
return 'You must register first by visiting <a href="' + registerUrl +
'" title="The section-watchlist registration page">the registration page</a>.';
}, function () {
return 'The server is down. Check ' + ENTERPRISEY_ENWP_TALK_PAGE_LINK + ' for updates.';
} );
}
// i.e. generate a message in the case that the backend gave us an error.
function generateBackendErrorMessage( backendResponse, registerUrl ) {
if( backendResponse.status === "bad_request" ) {
switch( backendResponse.data ) {
case "no_stored_token":
return "The system doesn't have a stored registration for your username. Please authenticate by visiting <a href='" + registerUrl + "' title='The section-watchlist registration page'>the registration page</a>.";
case "bad_token":
return "Authentication failed. Please re-authenticate by visiting <a href='" +
registerUrl + "'>the registration page</a>.";
}
}
return "Request failed (error: " + backendResponse.status + "/" + backendResponse.data +
"). Re-authenticating by visiting <a href='" + registerUrl + "'>the registration page</a> may help.";
}
function makeBackendQuery( query_path, callback ) {
var swtoken = mw.user.options.get( TOKEN_OPTION_NAME );
var registerUrl = BACKEND_URL + "/oauth-register?user_id=" + mw.config.get( "wgUserId" );
if( swtoken ) {
$.getJSON( BACKEND_URL + query_path + "&token=" + swtoken ).then( function ( response ) {
if( response.status === "success" ) {
callback( response.data );
$( "#mw-content-text" )
.append( "<div>(<div class='hlist hlist-separated inline'><ul id='section-watchlist-links'><li><a href='" + registerUrl + "'>re-register with backend</a></li></ul></div>)</div>" );
} else {
$( "#mw-content-text" ).html( generateBackendErrorMessage( response, registerUrl ) );
}
}, function () {
$( "#mw-content-text" ).html( CORS_ERROR_MESSAGE );
} );
} else {
generateNoTokenMessage( registerUrl ).then( function ( msg ) {
$( "#mw-content-text" ).html( msg );
} );
}
}
function showTabBackToWatchlist() {
// This tab doesn't get an access key because "L" already goes to the watchlist
var pageName = "Special:Watchlist";
var link = $( "<a>" )
.text( "Regular watchlist" )
.attr( "title", pageName )
.attr( "href", mw.util.getUrl( pageName ) );
$( "#p-namespaces ul" ).append(
$( "<li>" ).append( $( "<span>" ).append( link ) )
.attr( "id", "ca-nstab-regular-watchlist" ) );
}
mw.loader.using( [
"mediawiki.api",
"mediawiki.language",
"mediawiki.util",
"mediawiki.special.changeslist",
"mediawiki.special.changeslist.enhanced",
"mediawiki.interface.helpers.styles"
] ).then( function () {
var pageId = mw.config.get( "wgArticleId" );
var registerUrl = BACKEND_URL + "/oauth-register?user_id=" + mw.config.get( "wgUserId" );
if( mw.config.get( "wgPageName" ) === "Special:BlankPage/section-watchlist" ) {
var months = ( new mw.Api() ).loadMessages( englishMonths );
$( "#firstHeading" ).text( "Section watchlist" );
document.title = "Section watchlist - Wikipedia";
$( "#mw-content-text" ).empty();
makeBackendQuery( "/inbox?user_id=" + mw.config.get( "wgUserId" ), function ( data ) {
if( data.length ) {
var rendered = renderInbox( data );
$.when( months ).then( function () {
var renderedDays = rendered.map( function ( dayAndItems ) {
dayAndItems[1].reverse();
return [
$( "<h4>" ).text( renderIsoDate( dayAndItems[0] ) ),
$( "<ul>" ).append( dayAndItems[1] )
];
} );
renderedDays.reverse();
var elements = renderedDays.flat();
$( "#section-watchlist-links" ).prepend(
$( "<li>" ).append( $( "<a>", { "href": mw.util.getUrl( "Special:BlankPage/section-watchlist/edit" ) } ).text( "view list of watched sections" ) ) );
$( "#mw-content-text" ).append( elements );
mw.hook( "wikipage.content" ).fire( $( "#mw-content-text" ) );
} );
} else {
$( "#mw-content-text" ).text( "No edits yet!" );
}
} );
showTabBackToWatchlist();
} else if( mw.config.get( "wgPageName" ) === "Special:BlankPage/section-watchlist/edit" ) {
$( "#firstHeading" ).text( "Edit section watchlist" );
document.title = "Edit section watchlist - Wikipedia";
$( "#mw-content-text" )
.empty()
.append( $( "<p>" ).append( $( "<a>", { "href": mw.util.getUrl( "Special:BlankPage/section-watchlist" ) } ).text( "< Back to section watchlist" ) ) );
makeBackendQuery( "/all_subbed_sections?user_id=" + mw.config.get( "wgUserId" ), function ( data ) {
if( Object.keys( data ).length ) {
var list = $( "<ul>" ).appendTo( "#mw-content-text" );
Object.keys( data ).forEach( function ( pageId ) {
var pageData = data[pageId];
var listEl = $( "<li>" ).append( $( "<a>", { "href": mw.util.getUrl( pageData.title ) } ).text( pageData.title ) );
var sectionsList = $( "<ul>" ).appendTo( listEl );
pageData.sections.forEach( function ( section ) {
sectionsList.append( $( "<li>" ).append( $( "<a>", { "href": mw.util.getUrl( pageData.title ) + "#" + ( section[2] || section[0] ) } ).text( pageData.title + " § " + section[0].replace( /_/g, " " ) ) ) );
} );
list.append( listEl );
} );
//$( "#mw-content-text" ).append( elements );
//mw.hook( "wikipage.content" ).fire( $( "#mw-content-text" ) );
} else {
$( "#mw-content-text" ).text( "No subscribed sections yet!" );
}
} );
showTabBackToWatchlist();
} else if( mw.config.get( "wgAction" ) === "view" &&
pageId !== 0 &&
!window.location.search.includes( "oldid" ) ) {
registerUrl += "&return_page=" + encodeURIComponent( mw.config.get( "wgPageName" ) + window.location.hash );
if( mw.user.options.get( TOKEN_OPTION_NAME ) ) {
var allTranscludedTitles = getAllTranscludedTitles();
if( allTranscludedTitles.length ) {
$.when(
loadPagesWatched(),
api.get( {
action: "query",
prop: "info",
titles: allTranscludedTitles.join("|"),
inprop: "",
formatversion: 2
} )
).then( function ( pagesWatchedResult, infoQueryResult ) {
if( pagesWatchedResult.status === "success" ) {
var watchedPages = pagesWatchedResult.data;
var allTranscludedIds = infoQueryResult[0].query.pages.map( function ( page ) {
return page.pageid;
} );
var doesPageHaveWatchedSection = allTranscludedIds.some( function ( id ) {
return watchedPages.indexOf( String( id ) ) >= 0;
} );
var transcludedTitlesAndIds = infoQueryResult[0].query.pages.map( function ( page ) {
return { "title": page.title, "id": page.pageid };
} );
loadSectionsWatched( allTranscludedIds ).then( function ( sectionsWatchedResult ) {
if( sectionsWatchedResult.status === "success" ) {
initializeLinks( transcludedTitlesAndIds, sectionsWatchedResult.data );
} else {
console.error( "sectionsWatchedResult = ", sectionsWatchedResult );
initializeFakeLinks( generateBackendErrorMessage( sectionsWatchedResult, registerUrl ) );
}
}, function () {
console.error( "loadSectionsWatched failed, arguments = ", arguments );
initializeFakeLinks( CORS_ERROR_MESSAGE );
} );
} else {
console.error( "loadPagesWatched failed, pagesWatchedResult = ", pagesWatchedResult );
initializeFakeLinks( generateBackendErrorMessage( pagesWatchedResult, registerUrl ) );
}
}, function () {
initializeFakeLinks( CORS_ERROR_MESSAGE );
} );
}
} else {
// No stored token
generateNoTokenMessage( registerUrl ).then( function ( msg ) {
initializeFakeLinks( msg );
} );
}
} else if( mw.config.get( "wgPageName" ) === "Special:Watchlist" ) {
var pageName = "Special:BlankPage/section-watchlist";
var link = $( "<a>" )
.text( "Section watchlist" )
.attr( "accesskey", "s" )
.attr( "title", pageName )
.attr( "href", mw.util.getUrl( pageName ) );
link.updateTooltipAccessKeys();
$( "#p-namespaces ul" ).append(
$( "<li>" ).append( $( "<span>" ).append( link ) )
.attr( "id", "ca-nstab-section-watchlist" ) );
}
} );
} );