User:Enterprisey/section-watchlist.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:Enterprisey/section-watchlist. |
// 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" ) + "/enwiki/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" ) + "/enwiki/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" ) );
}
} );
} );