Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* WikiActivity
*
* Recreates the legacy Special:WikiActivity page on the Unified
* Community platform with a modernized appearance and a few
* upgrades.
*
* Note: This script is a community project and is high-traffic.
* If you know what you're doing and need to make any changes
* that will impact the users of the script, you are welcome
*
* Author: Ultimate Dark Carnage
* Version: v0.998b
**/
// Creating the configuration object if it does not exist
window.rwaOptions = window.rwaOptions || { };
// Initializing the script via an IIFE
( function( window, $, mw ) {
"use strict";
// The script name
const NAME = "WikiActivity";
// Current script version
const VERSION = "v1.0b";
// Creating the options object
const options = window.rwaOptions;
// Creating the MediaWiki configuration object
const conf = mw.config.get( [
"wgCityId",
"wgNamespaceNumber",
"wgFormattedNamespaces",
"wgTitle",
"wgSiteName",
"wgServer",
"wgUserName",
"wgUserGroups",
"wgScriptPath"
] );
// Creating the UCP object
window.UCP = window.UCP || { };
// Creating the Dev object
window.dev = window.dev || { };
// If this is not a special page or the script has been ran, do not run
if (
window.UCP.WikiActivity ||
window.WikiActivityLoaded
) return;
// Setting the loaded state to true
window.WikiActivityLoaded = true;
// Core scripts
const scripts = new Map( [
[ "i18n", "u:dev:MediaWiki:I18n-js/code.js" ],
[ "colors", "u:dev:MediaWiki:Colors/code.js" ],
[ "wds", "u:dev:MediaWiki:WDSIcons/code.js" ],
[ "ui", "u:dev:MediaWiki:UI-js/code.js" ]
] );
mw.loader.using( "mediawiki.util" ).then( function( ) {
// Importing required scripts
scripts.forEach( function( script, scriptName ) {
if ( window.dev[ scriptName ] ) return;
importArticle( { type : "script", article : script } );
} );
// Importing the core stylesheet
importArticle( {
type : "style",
article : "u:dev:MediaWiki:WikiActivity.css"
} );
} );
// Core MediaWiki dependencies
const deps = Object.freeze( [
"mediawiki.util",
"mediawiki.api"
] );
// This is a list of canonical limits
const LIMITS = Object.freeze( [
5,
10,
25,
50,
100,
250,
500
] );
// This is a list of namespaces to show
const SUPPORTED_NAMESPACES = Object.freeze( [
0, // Article (main)
1, // Article talk
2, // User
3, // User talk
4, // Project (or site name)
5, // Project talk
6, // File
7, // File talk
110, // Forum
111, // Forum talk
500, // User blog
501, // User blog comment
828, // Module
829 // Module talk
] );
// A list of talk namespaces
const IS_TALK = Object.freeze( [
3,
5,
111,
829
] );
// A list of comment namespaces
const IS_COMMENT = Object.freeze( Array.from( IS_TALK ).concat( 501 ) );
// Default configurations for the main object
const DEFAULTS = Object.freeze( {
// The limit of pages to show on the activity feed
limit : 50,
// A list of supported namespaces
namespaces : Array.from( SUPPORTED_NAMESPACES ).filter( function( n ) {
return conf.wgFormattedNamespaces.hasOwnProperty( n );
} ),
// Determines whether to initialize the script automatically
autoInit : true,
// Sets the WikiActivity theme
themeName : "main",
// Determines whether bot edits should be shown
showBotEdits : false,
// Determines whether the activity feed should be loaded on a module
loadModule : false,
// Allows for custom rendering (when a theme is set)
customRendering : { },
// Determines whether the Recent Changes link on the wiki
// header should be changed back to Wiki Activity.
// Note: This option has been set to false by default for sitewide use.
headerLink : false,
// Determines whether the activity feed should automatically refresh
refresh : false,
// Delay for refreshing the activity feed
// Default refresh period: 5 minutes
refreshDelay : 5 * 60 * 1000,
// The timeout for loading the activity feed
timeout : 10 * 1000
} );
// A list of canonical subpages
const SUBPAGES = Object.keys( [
// The main activity feed page
"main",
// Watched pages only
"watchlist",
// Feeds activity
"feeds",
// Media activity
"media"
] );
// A list of user groups that can block
const CAN_BLOCK = Object.freeze( [
"sysop",
"staff",
"wiki-specialist",
"soap",
"global-discussions-moderator"
] );
// A list of user groups that have moderator permissions
const IS_MOD = Object.freeze( Array.from( CAN_BLOCK )
.concat( "discussion-moderator", "threadmoderator" ) );
// A list of users that can rollback edits
const CAN_ROLLBACK = Object.freeze( Array.from( IS_MOD )
.concat( "rollback" ) );
// Escaping RegExp characters
function regesc( s ) {
return s.replace( /[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, "\\$&" );
}
// A cache of user avatars
const AVATAR_CACHE = new Map( );
// A group of icon names
const ICON_NAMES = new Map( [
[ "edit", "pencil" ],
[ "new", "add" ],
[ "comment", "comment" ],
[ "talk", "bubble" ],
[ "categorize", "tag" ],
[ "diff", "clock" ],
[ "options", "gear" ],
[ "more", "more" ]
] );
// Canonical activity types
const TYPES = new Map( [
[ "main", function( p, cont ) {
const params = {
action : "query",
format : "json",
list : "recentchanges",
rcprop : [
"comment",
"timestamp",
"user",
"title",
"userid",
"ids"
],
rctype : [
"categorize",
"edit",
"new"
],
rcstart : ( new Date( ) ).toISOString( ),
rcdir : "older",
rcshow : [ "!minor" ],
rclimit : p.limit,
rcnamespace : p.namespaces
};
if ( !p.allowBotEdits ) {
params.rcshow.push( "!bot" );
}
if ( cont ) {
params.rccontinue = cont;
delete params.rcstart;
}
return ( new mw.Api( ) ).post( params );
} ],
[ "watchlist", function( p, cont ) {
const params = {
action : "query",
format : "json",
list : "watchlist",
wlprop : [
"comment",
"timestamp",
"user",
"title",
"userid",
"ids"
],
wltype : [
"categorize",
"edit",
"new"
],
wlstart : ( new Date( ) ).toISOString( ),
wldir : "older",
wlshow : [ "!minor" ],
wllimit : p.limit,
wlnamespace : p.namespaces
};
if ( !p.allowBotEdits ) {
params.wlshow.push( "!bot" );
}
if ( cont ) {
params.wlcontinue = cont;
delete params.wlstart;
}
return ( new mw.Api( ) ).post( params );
} ],
/* [ "images", function( p, cont ) {
return ( new mw.Api( ) ).get( {
action : "query",
list : "logevents",
leprop : [
"title",
"userid",
"timestamp",
"comment"
],
ledir : "older",
lelimit : p.limit,
letype : "upload",
leaction : "upload/upload",
leend : ( new Date( ) ).toISOString( ),
format : "json"
} );
} ],*/
[ "feeds", function( p ) {
return new Promise( function( res, rej ) {
const req = new XMLHttpRequest( );
req.timeout = 30000;
req.addEventListener( "timeout", function( ) {
rej( "timeout" );
} );
req.addEventListener( "readystatechange", function( ) {
if ( req.readyState === req.DONE ) {
if ( req.status === 200 ) {
const data = req.response;
res( data._embedded[ "doc:posts" ] );
} else {
rej( "no-connection" );
}
}
} );
req.responseType = "json";
const servicesURL = "https://services.fandom.com";
const reqURL = new URL( "/discussion/" + conf.wgCityId + "/posts", servicesURL );
const showDeleted = p.storage.get( "showDeleted" );
const viewableOnly = ( p.isMod && !showDeleted ) ? true :
!p.isMod;
const params = new Map( [
[ "limit", p.limit ],
[ "viewableOnly", viewableOnly ]
] );
params.forEach( function( v, k ) {
reqURL.searchParams.append( k, v );
} );
req.open( "GET", reqURL, true );
req.setRequestHeader( "Accept", "application/hal+json" );
req.withCredentials = true;
req.send( );
} );
} ]
] );
// Aliases for activity types
const ALIASES = new Map( [
[ "discussions", "feeds" ],
[ "files", "images" ],
[ "following", "watchlist" ]
] );
// Parses the date object
function parseDate( x ) {
var d = null;
return !isNaN( (
d = ( x instanceof Date ) ?
x :
new Date( x )
)
) ?
d : null;
}
// Creating the storage constructor
function WAStorage( ) {
// Sets the instance to a variable
const ps = this;
// Creates the storage prefix
const psp = NAME + "-";
// Parses JSON safely
function safeJSONParse( s ) {
try { return JSON.parse( s ); }
catch ( e ) { console.warn( e ); return s; }
}
// Gets the storage key
function getStorageKey( k ) {
return arguments.length > 0 ? psp + k : "";
}
function watch( ) {
setInterval( function( ) {
const keys = Object.keys( localStorage )
.filter( function( s ) {
if ( !s.startsWith( psp ) ) return false;
const kv = localStorage.getItem( s );
const kp = safeJSONParse( kv );
return (
typeof kp === "object" &&
kp.hasOwnProperty( "expiry" ) &&
kp.hasOwnProperty( "value" )
);
} );
Array.from( keys ).forEach( function( key ) {
const value = safeJSONParse( localStorage.getItem( key ) );
const expiry = value.expiry;
const date = parseDate( expiry ).getTime( ) / 1000;
const curr = Date.now( ) / 1000;
const diff = date - curr;
if ( diff < 1 ) localStorage.removeItem( key );
} );
}, 1000 );
}
// Gets the value from its key
ps.get = function( k ) {
const pk = getStorageKey( k );
if ( pk === "" ) return null;
const pv = localStorage.getItem( pk );
return safeJSONParse( pv );
};
ps.set = function( k, v ) {
if (
arguments.length === 1 &&
typeof k === "object"
) {
return Object.keys( k ).some( function( key ) {
const value = k[ key ];
return ps.set( key, value );
} );
}
const pk = getStorageKey( k );
if ( pk === "" ) return false;
const psr = JSON.stringify( v );
localStorage.setItem( pk, psr );
};
ps.remove = function( k ) {
const pk = getStorageKey( k );
localStorage.removeItem( sk );
};
ps.clear = function( ) {
const k = Object.keys( localStorage ).filter( function( s ) {
return s.startsWith( psp );
} );
k.forEach( localStorage.removeItem );
};
ps.store = function( o, d ) {
if (
!isNaN( d ) &&
isFinite( d )
) {
d = Math.abs( parseInt( d ) );
d = Date.now( ) + d;
} else {
d = parseDate( d );
if ( isNaN( d ) ) d = Date.now( ) * ( 2 * 24 * 3600 * 1000 );
}
d = parseDate( d );
Object.keys( o ).forEach( function( k ) {
const v = o[ k ];
const r = new Map( [
[ "expiry", d.toISOString( ) ],
[ "value", v ]
] );
ps.set( k, Object.fromEntries( r ) );
} );
};
ps.fetch = function( k ) {
const pv = ps.get( k );
if (
typeof pv === "object" &&
pv.hasOwnProperty( "expiry" ) &&
pv.hasOwnProperty( "value" )
) {
return pv.value;
} else {
return pv;
}
};
watch( );
}
// Checks if the value is a plain object
function isPlainObject( o ) {
const ts = Object.prototype.toString;
return ts.call( o ) === "[object Object]";
}
// Creating the Recent Wiki Activity constructor
function WikiActivity( o, i18n ) {
// If the constructor was initialized without using the
// 'new' keyword, initialize the constructor anyway.
if ( this.constructor !== WikiActivity ) {
return new WikiActivity( o, i18n );
}
// Setting the current instance to a simple constant
const p = this;
// Title parts for WikiActivity
const parts = conf.wgTitle.split( "/" );
// The current subpage
const subpage = parts[ 1 ] || "";
// The custom wrapper for localStorage
const storage = new WAStorage( );
// Subpage patterns
const subpagePatterns = new Map( );
// Subpage keys
const subpageKeys = Array
.from( TYPES.keys( ) )
.filter( function( k ) {
return k !== "main";
} );
// Creating a helper function to create the subpage pattern
function getSubpagePattern( type ) {
const subpageName = i18n.msg( "page-" + type + "-subpage" )
.plain( );
const regexString = "^" + regesc( subpageName ) + "$";
return new RegExp( regexString, "i" );
}
// Adding keys to subpage patterns
const mt = { };
subpageKeys.forEach( function( k ) {
mt[ k ] = [ k ];
} );
ALIASES.forEach( function( k, a ) {
if ( !mt[ k ] ) mt[ k ] = [ k ];
mt[ k ].push( a );
} );
Object.keys( mt ).forEach( function( k ) {
const v = mt[ k ].map( getSubpagePattern );
subpagePatterns.set( k, v );
} );
function getType( s ) {
const e = Array.from( subpagePatterns.keys( ) );
const r = e.find( function( k ) {
const v = subpagePatterns.get( k );
const f = v.some( function( h ) {
return h.test( s );
} );
return f;
} );
if ( !r ) return "main";
return r;
}
function isMember( groups ) {
if ( Array.isArray( groups ) ) {
const g = Array.from( groups ).flat( Infinity );
return g.some( isMember );
} else {
return conf.wgUserGroups.includes( groups );
}
}
function timeago( x, p, calcDiff ) {
const diff = calcDiff ? (
Math.floor( Date.now( ) ) -
( new Date( x * 1000 ) )
) / 1000 : Number( x );
if ( isNaN( diff ) ) return "";
const minutes = Math.round( diff / 60 );
const hours = Math.round( minutes / 60 );
const days = Math.round( hours / 24 );
const months = Math.round( days / 30 );
const years = Math.round( months / 12 );
const prefix = "timeago-", suffix = "-ago";
if ( diff < 5 ) {
return p.msg( prefix + "justnow" ).parse( );
} else if ( diff < 60 ) {
return p.msg( prefix + "seconds" + suffix, diff ).parse( );
} else if ( minutes < 2 ) {
return p.msg( prefix + "minute" + suffix ).parse( );
} else if ( minutes < 60 ) {
return p.msg( prefix + "minutes" + suffix, minutes ).parse( );
} else if ( hours < 2 ) {
return p.msg( prefix + "hour" + suffix ).parse( );
} else if ( hours < 24 ) {
return p.msg( prefix + "hours" + suffix, hours ).parse( );
} else if ( days < 2 ) {
return p.msg( prefix + "day" + suffix ).parse( );
} else if ( days < 30 ) {
return p.msg( prefix + "days" + suffix, days ).parse( );
} else if ( months < 2 ) {
return p.msg( prefix + "month" + suffix ).parse( );
} else if ( months < 12 ) {
return p.msg( prefix + "months" + suffix, months ).parse( );
} else if ( years < 2 ) {
return p.msg( prefix + "year" + suffix ).parse( );
} else {
return p.msg( prefix + "years" + suffix, years ).parse( );
}
}
// Sets default options
p.options = Object.assign( { }, DEFAULTS );
// Setting the storage to the current instance
p.storage = storage;
// Determines whether the script has been loaded
p.loaded = false;
// Determines whether the script is loading
p.loading = false;
// Determines whether the user can block
p.canBlock = isMember( CAN_BLOCK );
// Determines whether the user has moderator permissions
p.isMod = p.canDelete = isMember( IS_MOD );
// Creating a shortcut to i18n.msg
p.msg = function( ) {
const a = Array.from( arguments );
const i18nWA = p.i18n;
return i18nWA.msg.apply( i18nWA, a );
};
// Determines whether the user has rollback permissions
p.canRollback = isMember( CAN_ROLLBACK );
// The I18n object
p.i18n = i18n;
// The current activity type
p.type = getType( subpage );
// Fetches the time difference
p.getTimeDiff = function( x ) {
const d = parseDate( x );
if ( d === null ) return d;
const t = Date.now( );
return Math.floor( ( t - d ) / 1000 );
};
// Configures all properties
p.configure = function( ) {
if ( arguments.length === 0 ) return null;
const r = Object( arguments[ 0 ] );
Object.keys( r ).forEach( function( k ) {
const g = p.options[ k ], v = r[ k ];
if ( v === void 0 ) p.options[ k ] = g;
else p.options[ k ] = v;
} );
};
// Gets the WikiActivity URL
p.getURL = function( ) {
// The "Special" namespace
const ns = conf.wgFormattedNamespaces[ -1 ];
// The "Wiki Activity" title
const title = p.i18n
.inContentLang( )
.msg( "page-title" )
.plain( );
// Returns the WikiActivity URL
return mw.util.getUrl( ns + ":" + title );
};
// Checks if the page matches
p.matchPage = function( ) {
// The primary page title
const title = p.i18n
.inContentLang( )
.msg( "page-title" )
.plain( );
// Checks whether the page is a special page
const isSpecial = conf.wgNamespaceNumber === -1;
// Title parts
const parts = conf.wgTitle.split( "/" );
// The main title
const mainTitle = parts[ 0 ];
// Returns the boolean value
return ( title === mainTitle || "WikiActivity" === mainTitle ) && isSpecial;
};
/* Core methods */
p.start = function( ) {
// Sets the MediaWiki config value to WikiActivity
mw.config.set( "wgCanonicalSpecialPageName", "WikiActivity" );
// The list of entries to load
p.entries = [ ];
// The length of entries
p.length = 0;
// Setting the target to the content area
p.target = document.querySelector( "#mw-content-text" );
// Loads all required resources
p.load = function( ) {
if ( arguments.length === 1 ) {
p.limit = Math.max( 5, Math.min( arguments[ 0 ], 500 ) );
if ( isNaN( p.limit ) ) p.limit = 50;
}
mw.hook( "ucp.wikiactivity.load" ).fire( p );
p.loading = true;
p.target.innerHTML = "";
Promise.all( [ "colors", "wds", "ui" ].map( function( k ) {
return new Promise( function( res, rej ) {
mw.hook( "dev." + k ).add( res );
} );
} ) ).then( p.init );
};
// Initializes the script
p.init = function( ) {
mw.hook( "ucp.wikiactivity.init" ).fire( p );
// Sets the refresh timeout to null by default
p.refreshTimeout = null;
// WDSIcons
p.wds = window.dev.wds;
// Colors
p.colors = window.dev.colors;
// UI-js
p.uijs = window.dev.ui;
// Creates the Activity Feed UI
p.ui = new FeedUI( p );
// Adding header tabs
const tabOrder = Object.freeze( [
"main",
"watchlist",
"feeds",
"images"
] );
const tabItems = Array
.from( tabOrder )
.sort( function( a, b ) {
const ai = tabOrder.indexOf( a );
const bi = tabOrder.indexOf( b );
return ai - bi;
} );
p.ui.addHeaderTabs( tabItems );
// Adds the loading spinner
p.ui.addSpinner( );
// Overwrites the document and header titles
p.overwriteTitle( );
// Sets the theme for WikiActivity
p.setTheme( );
// Loads data by type
p.loadData( );
};
// Fetches the key for the heading and document titles
p.getTitles = function( ) {
const type = p.type;
const o = { doc : "", heading : "" };
if ( type === "main" ) {
o.doc = "doc-title";
o.heading = "heading-title";
} else {
o.doc = "doc-" + type + "-title";
o.heading = "doc-" + type + "-heading";
}
return o;
};
// Overwrites existing titles
p.overwriteTitle = function( ) {
// Title key
const title = p.getTitles( );
// Changes the original document title to
// the WikiActivity title
document.title = p.msg( title.doc, conf.wgSiteName )
.escape( );
// Changes the original heading title to the
// WikiActivity title
document.querySelector( ".page-header__title" )
.textContent = p.msg( title.heading ).escape( );
};
// Gets the current theme name
p.getTheme = function( ) {
return typeof p.themeName === "string" ? p.themeName : "main";
};
// Sets the theme
p.setTheme = function( ) {
// Fetches the theme name
const themeName = p.getTheme( );
// Sets the theme name
p.themeName = themeName;
// Sets the theme to the data attribute
p.ui.setTheme( themeName );
// Checks if the page color is bright
const isBright = p.colors.parse( p.colors.fandom.page )
.isBright( );
// Creates the initial theme object
p.theme = { };
// Link color
p.theme.link = p.colors.parse( p.colors.fandom.link );
// Background color
p.theme.backgroundColor = p.colors.parse( p.colors.fandom.page )
.lighten( 45 * ( isBright ? 1 : -1 ) );
// Border color for edited changes
p.theme.edit = p.colors.parse( "#feaf09" );
// Border color for comments
p.theme.comment = p.colors.parse( "#dddddd" );
// Border color for new pages
p.theme[ "new" ] = p.colors.parse( "deepskyblue" );
// Border color for new categories
p.theme.categorize = p.colors.parse( "#b76801" );
// Creates a new stylesheet
p.colors.css( ":root { " +
"--wa-edit: $edit;" +
"--wa-comment: $comment;" +
"--wa-new: $new;" +
"--wa-categorize: $categorize;" +
"--wa-background__color: $backgroundColor;" +
"--wa-link__color: $link;" +
"}", p.theme );
};
// Fetches the proper ajax for a certain type
p.getAjax = function( ) {
var a;
if ( TYPES.has( p.type ) ) {
a = TYPES.get( p.type );
} else {
a = TYPES.get( "main" );
}
return a( p );
};
// Fetches the proper ajax for a certain type
p.getAjaxCont = function( cont ) {
var a;
if ( TYPES.has( p.type ) ) {
a = TYPES.get( p.type );
} else {
a = TYPES.get( "main" );
}
return a( p, cont );
};
// Loads data for all changes
p.loadData = function( ) {
mw.hook( "ucp.wikiactivity.load" ).fire( p );
// Main and watchlist types
const mainWLTypes = Object.freeze( [
"main",
"watchlist"
] );
// If the activity type is main or watchlist,
// handle data the regular way
if ( mainWLTypes.includes( p.type ) ) {
p.loadChanges( );
} else if ( p.type === "feeds" ) {
p.loadFeeds( );
} else if ( p.type === "images" ) {
p.loadImageUpload( );
} else {
p.type = "main";
p.loadChanges( );
}
};
// Loads more data for all changes
p.loadMore = function( ) {
// Main and watchlist types
const mainWLTypes = Object.freeze( [
"main",
"watchlist"
] );
// If the activity type is main or watchlist,
// handle data the regular way
if ( mainWLTypes.includes( p.type ) ) {
p.loadMoreChanges( );
} else if ( p.type === "feeds" ) {
p.loadMoreFeeds( );
} else if ( p.type === "images" ) {
p.loadMoreImageUpload( );
} else {
p.type = "main";
p.loadMoreChanges( );
}
};
/* Recent Changes and Watchlist */
// Loads data from recent changes or watchlist
p.loadChanges = function( ) {
// Fetches ajax from recent changes or watchlist
const ajax = p.ajax = p.getAjax( );
// Creates a new promise for handling queries
const qp = new Promise( function( res, rej ) {
ajax.then( p.handleChanges )
.then( res )
[ "catch" ]( rej );
} );
qp.then( p.handleData )
[ "catch" ]( p.handleLoadException );
};
// Handles results from changes
p.handleChanges = function( response ) {
if ( response.error ) throw new ReferenceError( resposne );
// Current key for types
const k = p.type === "main" ? "recentchanges" : p.type;
// Fetches the query
const query = response.query, result = query[ k ];
// Continue prefix
const cp = p.type === "main" ? "rc" : "wl";
// Continue key
const ck = cp + "continue";
// Continue object
const contO = response.continue || null;
// Continue string
const cont = contO && contO[ ck ];
if ( contO && cont ) {
p._hasContinue = true;
p._continue = cont;
} else {
p._hasContinue = false;
}
return result;
};
// Loads more changes when triggered
p.loadMoreChanges = function( ) {
// If no continue string is found, do not run
if ( !p._hasContinue ) return;
// Fetches ajax from recent changes or watchlist
const ajax = p.ajax = p.getAjaxCont( p._continue );
// Creates a new promise for handling queries
const qp = new Promise( function( res, rej ) {
ajax.then( p.handleChanges )
.then( res )
[ "catch" ]( rej );
} );
qp.then( p.handleData )
[ "catch" ]( p.handleLoadException );
};
// Handles data from recent changes or watchlist
p.handleData = function( v ) {
Promise
.all( Array.from( v ).map( p.loadEntryData ) )
.then( p.render );
};
// Loads data from individual recent changes entries
p.loadEntryData = function( entry ) {
const params = new Map( [
[ "user", entry.user ],
[ "title", entry.title ],
[ "userid", entry.userid ],
[ "revid", entry.revid ],
[ "oldid", entry.old_revid ],
[ "summary", entry.comment ],
[ "ns", entry.ns ],
[ "time", entry.timestamp ],
[ "isComment", IS_COMMENT.includes( entry.ns ) ],
[ "isTalk", IS_TALK.includes( entry.ns ) ]
] );
if ( params.get( "isComment" ) ) {
params.set( "type", "comment" );
params.set( "trueType", entry.type );
} else {
params.set( "type", entry.type );
params.set( "trueType", entry.type );
}
if ( params.get( "trueType" ) === "new" ) {
const w = p.msg( "created-with" ).escape( );
const r = params.get( "summary" ).startsWith( w );
if ( r ) {
const l = w.length;
const s = params.get( "summary" );
const u = s.slice( l )
.trim( )
.replace( /^\"\s*|\s*\"$/g, "" )
.trim( );
params.set( "summary", u );
}
}
const c = p.parseSummary( params.get( "summary" ) );
const d = p.getTimeDiff( params.get( "time" ) );
params.set( "summary", c.trim( ) );
params.set( "timeDiff", d );
const esp = /^\/\*\s*(.*)\s*\*\/$/;
const e = esp.test( params.get( "summary" ) );
params.set( "editedSection", e );
if ( params.get( "editedSection" ) ) {
const t = params.get( "summary" );
const v = t.replace( esp, "$1" ).trim( );
params.set( "type", "edited-section" );
params.set( "summary", v );
}
return p.generateRevisions( params );
};
// If there is no old revision ID, proceed with generating an individual entry
p.generateRevisions = function( params ) {
const f = params.get( "oldid" ) === 0 ?
"generateEntryData" :
"compareRevisions";
return p[ f ]( params );
};
// Compares revisions and finds data for each revision
p.compareRevisions = function( params ) {
const oldid = params.get( "oldid" );
const revid = params.get( "revid" );
const revids = [ oldid, revid ];
const ajax = p.compareRevisionsAjax( revids );
return new Promise( function( res, rej ) {
ajax.then( function( response ) {
const query = response.query;
const missing = query.pages.hasOwnProperty( "-1" );
if ( missing ) {
return p.generateEntryData( params ).then( res );
}
const pageid = query.pageids[ 0 ];
const page = query.pages[ pageid ];
const rev = page.revisions;
if ( rev.length === 0 ) {
return p.generateEntryData( params ).then( res );
}
const or = rev.find( function( r ) {
return r.revid === oldid;
} );
const nr = rev.find( function( r ) {
return r.revid === revid;
} );
const omain = or.slots.main;
const nmain = nr.slots.main;
const owikitext = omain.contentmodel === "wikitext";
const nwikitext = nmain.contentmodel === "wikitext";
if ( owikitext && nwikitext ) {
const oc = omain[ "*" ];
const nc = nmain[ "*" ];
const catp = new RegExp(
"\\[\\[(" + conf.wgFormattedNamespaces[ 14 ] +
":[^\\[\\]\\|]+)(?:\\s*\\|.*)?\\]\\]", "g"
);
const filp = new RegExp(
"\\[\\[(" + conf.wgFormattedNamespaces[ 6 ] +
":[^\\[\\]\\|]+)(?:\\s*\\|.*)?\\]\\]", "g"
);
const tp = new RegExp(
"\\{\\{([^\\{\\}\\|]+)(?:\\|.*)?\\}\\}", "g"
);
const ocat = ( oc.match( catp ) || [ ] )
.map( function( n ) {
return n.replace( catp, "$1" );
} );
const ncat = ( nc.match( catp ) || [ ] )
.map( function( n ) {
return n.replace( catp, "$1" );
} );
const ofil = ( oc.match( filp ) || [ ] )
.map( function( n ) {
return n.replace( filp, "$1" );
} );
const nfil = ( nc.match( filp ) || [ ] )
.map( function( n ) {
return n.replace( filp, "$1" );
} );
const otem = ( oc.match( tp ) || [ ] )
.map( function( n ) {
return n.replace( tp, "$1" );
} );
const ntem = ( nc.match( tp ) || [ ] )
.map( function( n ) {
return n.replace( tp, "$1" );
} );
const categories = ncat.filter( function( n ) {
return !ocat.includes( n );
} );
const files = nfil.filter( function( n ) {
return !ofil.includes( n );
} );
const templates = ntem.filter( function( n ) {
return !otem.includes( n );
} );
params.set( "categories", categories );
if ( params.get( "categories" ).length ) {
params.set( "type", "categorize" );
}
params.set( "templates", templates );
if ( files.length > 0 ) {
p.fetchImageInfo( files, params, res );
} else {
params.set( "images", [ ] );
p.handleEntryData( params, res );
}
} else {
return p.generateEntryData( params ).then( res );
}
} );
} );
};
// Ajax for comparing revisions to each other
p.compareRevisionsAjax = function( revids ) {
return new Promise( function( res, rej ) {
return ( new mw.Api( ) ).post( {
action : "query",
prop : "revisions",
rvprop : [ "content", "ids" ],
rvslots : "*",
revids : revids,
indexpageids : true
} ).done( res ).fail( rej );
} );
};
// Parses the summary
p.parseSummary = function( s ) {
s = mw.html.escape( s );
const replacers = new Map( [
[ /\[\[([^\[\]\|]+)\|([^\[\]]+)\]\]/g, function( m, r1, r2 ) {
const a = document.createElement( "a" );
const url = mw.util.getUrl( r1 );
a.setAttribute( "href", url );
a.innerText = r2;
return a.outerHTML;
} ],
[ /\[\[([^\[\]]+)\]\]/g, function( m, r1 ) {
const a = document.createElement( "a" );
const url = mw.util.getUrl( r1 );
a.setAttribute( "href", url );
a.innerText = r1;
return a.outerHTML;
} ]
] );
replacers.forEach( function( value, pattern ) {
s = s.replace( pattern, value );
} );
return s;
};
// Generates the required AJAX object for
// generating entry data.
p.generateDataAjax = function( revid ) {
return new Promise( function( res, rej ) {
( new mw.Api( ) ).post( {
action : "query",
prop : [
"categories",
"images"
].join( "|" ),
revids : revid,
indexpageids : true,
format : "json"
} ).done( res ).fail( rej );
} );
};
// Generates data from a feed entry
p.generateEntryData = function( params ) {
const revid = params.get( "revid" );
const ajax = p.generateDataAjax( revid );
return new Promise( function( res, rej ) {
ajax.then( function( d ) {
const pageid = d.query.pageids[ 0 ];
const missing = d.query.hasOwnProperty( "-1" );
if ( missing ) {
p.handleEntryData( params, res );
} else {
const page = d.query.pages[ pageid ];
p.handleEntryInfo( page, params, res );
}
} );
} );
};
// Handles info fetched from the feed entry
p.handleEntryInfo = function( page, params, callback ) {
const categories = Array.from( page.categories || [ ] )
.map( function( c ) { return c.title; } );
const files = Array.from( page.images || [ ] )
.map( function( f ) { return f.title; } );
params.set( "categories", categories );
p.fetchTemplates( files, params, callback );
};
// Fetches templates from the page
p.fetchTemplates = function( files, params, callback ) {
const ajax = p.fetchTemplatesAjax( params );
ajax.done( function( response ) {
const query = response.query;
const missing = query.pages.hasOwnProperty( "-1" );
if ( !missing ) {
const pageid = query.pageids[ 0 ];
const page = query.pages[ pageid ];
const revisions = page.revisions;
if ( revisions.length === 0 ) {
params.set( "templates", [ ] );
} else {
const revision = revisions[ 0 ];
const main = revision.slots.main;
const content = main[ "*" ];
const tPattern = new RegExp(
"\\{\\{([^\\{\\}\\|]+)(?:\\|.*)?\\}\\}", "g"
);
const t = content.match( tPattern ) || [ ];
const templates = t.map( function( n ) {
return n.replace( tPattern, "$1" );
} );
params.set( "templates", templates );
}
} else {
params.set( "templates", [ ] );
}
if ( files.length ) {
// If any images exists, get info
p.fetchImageInfo( files, params, callback );
} else {
// Otherwise, immediately handle the entry data
params.set( "images", [ ] );
p.handleEntryData( params, callback );
}
} );
};
// Ajax for fetching templates from the page content
p.fetchTemplatesAjax = function( params ) {
return ( new mw.Api( ) ).post( {
action : "query",
prop : "revisions",
rvprop : "content",
rvslots : "*",
revids : params.get( "revid" ),
indexpageids : true,
format : "json"
} );
};
// Fetches the required AJAX for fetching image info
p.fetchImageInfoAjax = function( files ) {
return new Promise( function( res, rej ) {
return ( new mw.Api( ) ).post( {
action : "query",
prop : "imageinfo",
iiprop : "url",
titles : files,
indexpageids : true,
format : "json"
} ).done( res ).fail( rej );
} );
};
// Fetches image file URL and source from files
p.fetchImageInfo = function( files, params, callback ) {
const ajax = p.fetchImageInfoAjax( files );
ajax.then( function( d ) {
const pageids = d.query.pageids;
const missing = d.query.pages[ "-1" ];
if ( missing ) {
params.set( "images", [ ] );
} else {
const images = Array.from( pageids ).map( function( pageid ) {
const file = d.query.pages[ pageid ];
const fileParams = new Map( [
[ "src", file.imageinfo[ 0 ].url ],
[ "url", file.imageinfo[ 0 ].descriptionurl ],
[ "title", file.title ]
] );
return fileParams;
} );
params.set( "images", images );
}
// Now we can handle the data from our entry
p.handleEntryData( params, callback );
} );
};
// Handle an individual entry and inserts them into the array
p.handleEntryData = function( params, callback ) {
// Fetches the user's avatar
p.fetchAvatar( params )
.then( function( avatar ) {
params.set( "avatar", avatar );
return p.addEntry( params );
} )
.then( callback );
};
// Inserts an entry into the array
p.addEntry = function( params ) {
const entry = p.makeFeedEntry( params );
p.length = p.entries.push( entry );
return entry;
};
// Fetches the name of the icon
p.fetchIconName = function( type ) {
if ( ICON_NAMES.has( type ) ) {
return type;
} else {
return "edit";
}
};
// Makes feed entry
p.makeFeedEntry = function( params ) {
const type = params.get( "type" );
const iconName = p.fetchIconName( type );
params.set( "icon", iconName );
const editedKey = ( type === "new" ) ?
"created" :
"edited";
params.set( "editedKey", editedKey );
const timediff = params.get( "timeDiff" );
const t = timeago( timediff, p );
params.set( "timeago", t );
params.set( "hasDiff", params.get( "oldid" ) !== 0 );
const isTalk = params.get( "isTalk" );
const skm = new Map( [
[ "edited-section", ( isTalk && type === "edit" ) ],
[ "new-page", type === "new" ],
[ "summary", true ] // default key
] );
const sk = Array.from( skm.keys( ) ).find( function( k ) {
const c = skm.get( k );
return c === true;
} );
params.set( "summaryKey", sk );
return params;
};
// Generates the avatar from the user who made the edit
p.fetchAvatar = function( params ) {
const userid = params.get( "userid" );
return new Promise( function( res, rej ) {
// Inserts the avatar into the cache if it does not exist
if ( AVATAR_CACHE.has( userid ) ) {
res( AVATAR_CACHE.get( userid ) );
} else {
const ajax = p.generateAvatar( userid );
ajax.then( function( a ) {
AVATAR_CACHE.set( userid, a );
res( a );
} );
}
} );
};
// Creating required AJAX for generating an avatar
p.fetchUserDataAjax = function( userid ) {
return $.post( mw.util.wikiScript( "wikia" ), {
controller : "UserProfile",
method : "getUserData",
userId : userid,
format : "json"
} );
};
// Fetches the user data from AJAX
p.generateAvatar = function( userid ) {
return new Promise( function( res, rej ) {
p.fetchUserDataAjax( userid )
.done( function( d ) {
const user = d.userData;
res( user.avatar );
} );
} );
};
// Handles exception for loading recent changes
p.handleRCLoadException = function( exception ) {
p.renderException( "failed", exception );
};
// Renders activity feed
p.render = function( ) {
if (
p.themeName !== "main" ||
p.themeName !== void 0
) {
p.ui.setTheme( p.themeName );
}
// Main and watchlist types
const mainWLTypes = Object.freeze( [
"main",
"watchlist"
] );
// If the activity type is main or watchlist,
// handle data the regular way
if ( mainWLTypes.includes( p.type ) ) {
p.renderChanges( );
} else if ( p.type === "feeds" ) {
p.renderFeeds( );
} else if ( p.type === "images" ) {
p.renderImages( );
} else {
p.renderChanges( );
}
mw.hook( "ucp.wikiactivity.render" ).fire( p, p.ui );
p.loading = false;
p.loaded = true;
if ( p.refresh ) {
p.refreshTimeout = setTimeout( function( ) {
clearTimeout( p.refreshTimeout );
p.refreshTimeout = null;
p.ui.addSpinner( );
p.load( );
}, p.refreshDelay );
}
};
// Renders recent changes and watchlist
p.renderChanges = function( ) {
const entries = p.entries.sort( function( a, b ) {
return a.get( "timeDiff" ) - b.get( "timeDiff" );
} );
p.ui.addEntries( entries );
p.ui.appendTo( p.wrapper );
p.loading = false;
p.loaded = true;
};
// Render feeds
p.renderFeeds = function( ) {
const entries = p.entries.sort( function( a, b ) {
return a.get( "timeDiff" ) - b.get( "timeDiff" );
} );
p.ui.addFeedsEntries( entries );
p.ui.appendTo( p.wrapper );
p.loading = false;
p.loaded = true;
};
// Renders an exception for WikiActivity
p.renderException = function( name, exception ) {
const s = p.msg( "rwa-exception-" + ( name || "main" ), exception ).escape( );
console.error( s );
p.wrapper.innerHTML = s;
};
// Creates a rail module for WikiActivity
p.createModule = p.createRailModule = function( opts ) {
opts = p.configureModuleOptions( opts );
p.ui.createModule( opts );
};
// Configuring options for the modules
p.configureModuleOptions = function( o ) {
const r = Object.freeze( {
id : "",
classes : [ ],
background : true,
title : "",
content : ""
} );
o = Object( o );
const n = { };
Object.keys( o ).forEach( function( k ) {
n[ k ] = o[ k ] !== void 0 ? o[ k ] : r[ k ];
} );
return n;
};
/* Feeds */
p.loadFeeds = function( ) {
// Checks if the Message Walls are enabled
p.wallsEnabled = false;
// Gets Services API ajax for feeds
const t = TYPES.get( p.type );
const ajax = t.call( p, p );
// Ajax for checking if Message Walls exists
const wajax = p.wallsEnabledAjax( );
// Creates a new promise for handling data
const qp = new Promise( function( res, rej ) {
ajax.then( res )
[ "catch" ]( rej );
} );
// Checks if message walls exists
const we = new Promise( function( res, rej ) {
wajax.then( function( ) {
p.wallsEnabled = true;
} )[ "finally" ]( res );
} );
// Then, handle data from feeds
we.then( function( ) {
qp.then( p.handleFeedsData )
[ "catch" ]( p.handleLoadException );
} );
};
// Checks if Message Walls exists
p.wallsEnabledAjax = function( ) {
return new Promise( function( res, rej ) {
const req = new XMLHttpRequest( );
req.timeout = 30000;
req.addEventListener( "timeout", rej );
req.addEventListener( "readystatechange", function( ) {
if ( req.readyState === req.DONE ) {
if ( req.status === 200 ) res( );
rej( );
}
} );
const u = mw.util.getUrl( "Message Wall:Wikia" );
req.open( "GET", u, true );
req.send( );
} );
};
// Handles Feeds data
p.handleFeedsData = function( d ) {
Promise
.all( Array.from( d ).map( p.getFeedsEntry ) )
.then( p.render );
};
// Loads Feeds entry
p.getFeedsEntry = function( post ) {
return new Promise( function( res, rej ) {
const params = new Map( [
[ "title", mw.html.escape( post._embedded.thread[ 0 ].title || "" ) ],
[ "text", mw.html.escape( ( post.rawContent.length > 250 ?
post.rawContent.substring( 0, 250 ).trim( ) + "..." :
post.rawContent ) || "" ) ],
[ "username", mw.html.escape( post.createdBy.name || "" ) ],
[ "userid", post.createdBy.id ],
[ "avatar", post.createdBy.avatarUrl ],
[ "time", timeago( post.creationDate.epochSecond, p, true ) ],
[ "images", post._embedded.contentImages ],
[ "embed", post._embedded.openGraph ],
[ "messageId", post.id ],
[ "threadId", post.threadId ],
[ "forumName", mw.html.escape( post.forumName || "" ) ],
[ "forumId", post.forumId ],
[ "isReply", post.isReply ],
[ "tags", [ ] ],
[ "isReported", ( p.isMod ? post.isReported : null ) ],
[ "isLocked", post._embedded.thread[ 0 ].isLocked ],
[ "isDeleted", null ],
[ "isMessageDeleted", post.isDeleted ],
[ "isThreadDeleted", post._embedded.thread[ 0 ].isDeleted ]
] );
console.log( params );
const username = params.get( "username" );
params.set(
"usernameEncoded",
username.replace( /\s+/g, "_" )
);
const avatar = params.get( "avatar" );
if ( !avatar ) {
const defAvatar = 'https://vignette.wikia.nocookie.net/messaging/images/1/19/Avatar.jpg/revision/latest';
params.set( "avatar", defAvatar );
} else if ( avatar.includes( "/messaging/" ) ) {
const finalAvatarURL = avatar
.replace( /[^jpg]*$/, "" ) +
'/revision/latest';
params.set( "avatar", finalAvatarURL );
}
const isThreadDeleted = params.get( "isThreadDeleted" );
const isMessageDeleted = params.get( "isMessageDeleted" );
const isDeleted = Boolean( isThreadDeleted || isMessageDeleted );
params.set( "isDeleted", isDeleted );
if ( isMessageDeleted ) {
params.set( "deleter", post.lastDeletedBy.name );
params.set( "deleterId", post.lastDeletedBy.id );
}
if (
!post.isReply &&
post._embedded &&
post._embedded.thread &&
post._embedded.thread[ 0 ] &&
post._embedded.thread[ 0 ].tags
) {
const tags = post._embedded.thread[ 0 ].tags;
const t = Array.from( params.get( "tags" ) || [ ] );
tags.forEach( function( tag ) {
console.log( tag );
t.push( {
title : mw.html.escape( tag.articleTitle )
} );
} );
params.set( "tags", t );
}
p.addEntry( params );
res( params );
} );
};
/* Image Upload (not ready yet) */
/* p.loadImageUpload = function( ) {
const t = TYPES.get( p.type );
const ajax = t.call( p, p );
const qp = new Promise( function( res, rej ) {
ajax.then( function( d ) {
if ( d.error ) return rej( "notfound", d.error );
} );
} );
}; */
p.load( );
};
/* Fallback methods */
// Initializing the fallback
p.startFallback = function( ) {
// If the page matches, do not continue
if ( p.matchPage( ) ) return;
// Adds a link to the toolbar
p.addLink = function( ) {
// The tool item to the WikiActivity page
const li = document.createElement( "li" );
// The link for the WikiActivity page
const a = document.createElement( "a" );
// Inserts the link to the tool item
li.append( a );
// Inserts the overflow class
li.classList.add( "overflow" );
// The link title
const linkTitle = p.msg( "link-title" ).plain( );
// Page suffix
const pageSuffix = p.i18n
.inContentLang( )
.msg( "page-title" )
.plain( );
// The page title
const pageTitle = conf.wgFormattedNamespaces[ -1 ] + ":" +
pageSuffix;
// Setting up the link
a.setAttribute( "href", mw.util.getUrl( pageTitle ) );
a.textContent = linkTitle;
// Inserting the link to the tools menu
const tools = document.querySelector( "#WikiaBarWrapper .tools" );
tools.insertAdjacentElement( "afterbegin", li );
// After that, convert header link if it is allowed
if ( p.options.headerLink ) p.convertHeaderLink( );
};
p.convertHeaderLink = function( ) {
// If the user does not want the header links to be converted,
// do not continue.
if ( !p.options.headerLink ) return;
// The link target
const target = document.querySelector( ".wds-community-header__wiki-buttons [data-tracking='recent-changes']" );
// Changes the link to a wiki activity link
target.dataset.tracking = "wiki-activity";
target.setAttribute( "href", p.getURL( ) );
// Fetches the explore tab
const exploreLink = document.querySelector( '.wds-community-header__local-navigation [data-tracking="explore-changes"]' );
// Changes the explore link to an activity link
exploreLink.dataset.tracking = "explore-activity";
exploreLink.setAttribute( "href", p.getURL( ) );
exploreLink.textContent = p.msg( "link-title" ).plain( );
// If loading the module is allowed, continue
if ( p.options.loadModule ) p.loadModule( );
};
p.loadModule = function( ) { };
p.addLink( );
};
p.configure( o );
// Namespaces to show on the activity feed
p.namespaces = p.options.namespaces;
// Limit of entries to show on the feed
p.limit = p.options.limit;
// Determines whether the script should be automatically initialized
p.autoInit = p.options.autoInit;
// Determines how the activity feed gets rendered
p.customRendering = p.options.customRendering;
// Sets the WikiActivity theme
p.themeName = p.options.themeName;
// Determines whether bot edits should be shown
p.showBotEdits = p.options.showBotEdits;
// Determines whether the activity feed should be loaded on a module
p.loadModule = p.options.loadModule;
// Determines whether the Recent Changes link on the wiki
// header should be changed back to Wiki Activity.
// Note: This option has been set to false by default for sitewide use.
p.headerLink = p.options.headerLink;
// Determines whether the activity feed should automatically refresh
p.refresh = p.options.refresh;
// Delay for refreshing the activity feed
// Default refresh period: 5 minutes
p.refreshDelay = p.options.refreshDelay;
// The timeout for loading the activity feed
p.timeout = p.options.timeout;
if ( !p.matchPage( ) ){
p.startFallback( );
mw.hook( "ucp.wikiactivity.fallback" ).fire( p );
} else {
p.start( );
mw.hook( "ucp.wikiactivity.start" ).fire( p );
}
}
function FeedUI( p ) {
if ( this.constructor !== FeedUI ) {
return new FeedUI( p );
}
const u = this, limitCache = { };
function parseHTML( s ) {
const w = new DOMParser( );
const q = w.parseFromString( s, "text/html" );
return q.body.firstElementChild;
}
function safeJSONParse( s ) {
try {
return JSON.parse( s );
} catch( e ) {
return { };
}
}
function createElement( s, o ) {
const r = { }, q = Object( o );
if ( typeof s === "string" ) {
r.type = s;
}
if ( q.hasOwnProperty( "condition" ) ) {
if ( typeof q.condition === "function" ) {
const v = q.condition( );
r.condition = v;
} else {
r.condition = q.condition;
}
}
if (
q.hasOwnProperty( "attr" ) ||
q.hasOwnProperty( "attributes" )
) {
const attr = q.attr || q.attributes;
r.attr = attr instanceof Map ? Object.fromEntries( map ) :
attr;
}
if (
q.hasOwnProperty( "data" ) ||
q.hasOwnProperty( "dataset" )
) {
const data = q.data || q.dataset;
r.data = data instanceof Map ? Object.fromEntries( data ) :
Object( data );
}
if (
q.hasOwnProperty( "events" ) ||
q.hasOwnProperty( "on" )
) {
const evs = q.events || q.on;
r.events = evs instanceof Map ? Object.fromEntries( evs ) :
Object( evs );
}
if ( q.child ) {
r.children = [ q.child ];
} else if ( q.children ) {
r.children = q.children;
}
if (
q.hasOwnProperty( "classes" ) ||
q.hasOwnProperty( "classNames" )
) {
r.classes = q.classes || q.classNames;
} else if (
q.hasOwnProperty( "className" )
) {
r.classes = q.className.split( /\s+/g );
}
if ( q.html ) r.html = q.html;
if ( q.text ) r.text = q.text;
if ( q.parent ) {
r.parent = q.parent;
}
if ( q.hasOwnProperty( "props" ) ) {
const props = q.props;
r.props = props instanceof Map ? Object.fromEntries( props ) :
Object( props );
}
if ( q.checked ) r.checked = q.checked;
if ( q.selected ) r.selected = q.selected;
if ( q.value ) r.value = q.value;
return p.uijs( r );
}
u.createElement = createElement;
u.empty = function( el ) {
if ( arguments.length === 0 ) {
el = u.el;
} else if ( typeof el === "string" ) {
el = document.querySelector( el );
}
while ( el.firstChild ) {
el.removeChild( el.firstChild );
}
};
u.init = function( ) {
u.header = createElement( "header", {
classes : [ "FeedHeader" ],
attr : {
id : "FeedHeader"
}
} );
u.content = createElement( "div", {
classes : [ "FeedContent" ],
attr : {
id : "FeedContent"
}
} );
u.rail = createElement( "aside", {
classes : [ "FeedRail" ],
attr : {
id : "FeedRail"
}
} );
u.wrapper = createElement( "section", {
classes : [ "FeedContentWrapper" ],
attr : {
id : "FeedContentWrapper"
},
children : [
u.content,
u.rail
]
} );
u.el = createElement( "div", {
classes : [ "Feed" ],
attr : {
id : "Feed"
},
children : [
u.header,
u.wrapper
]
} );
p.target.append( u.el );
u.render( );
};
u.addSpinner = function( ) {
u.spinner = createElement( "svg", {
classes : [
"wds-spinner",
"wds-spinner__block"
],
attr : {
width : "66",
height : "66",
viewBox : "0 0 66 66",
xmlns : "http://www.w3.org/2000/svg"
},
children : [ {
type : "g",
attr : {
transform : "translate(33, 33)"
},
children : [ {
type : "circle",
classes : [
"wds-spinner__stroke"
],
attr : {
fill : "none",
"stroke-width" : "2",
"stroke-dasharray" : "188.49555921538757",
"stroke-dashoffset" : "188.49555921538757",
"stroke-linecap" : "round",
"r" : "30"
}
} ]
} ]
} );
u.empty( u.content );
u.content.append( u.spinner );
};
u.render = function( ) {
u.rcButton = createElement( "div", {
classes : [
"FeedRCButtonWrapper",
"FeedHeaderSection"
],
children : [ {
type : "a",
attr : {
href : mw.util.getUrl( "Special:RecentChanges" )
},
classes : [
"FeedRCButton",
"wds-button"
],
text : p.msg( "recentchanges-text" ).plain( )
} ]
} );
u.headerTabs = createElement( "nav", {
classes : [
"FeedTabsContainer",
"FeedHeaderSection"
]
} );
u.headerOptions = createElement( "nav", {
classes : [
"FeedOptions",
"FeedHeaderSection"
],
children : [
{
type : "div",
classes : [
"FeedDropdown"
],
events : {
click : function( ev ) {
const t = { };
if (
!ev.target.classList
.contains( "FeedDropdown" )
) {
t.el = ev.target.parentElement;
} else {
t.el = ev.target;
}
t.dropdown = t.el.nextElementSibling;
t.dropdown.classList.toggle( "active" );
}
},
children : [ u.getIcon( "options" ) ]
},
{
type : "section",
classes : [
"FeedDropdownContent"
],
children : [
{
type : "div",
classes : [
"FeedLimitContainer",
"FeedSelect"
],
children : [
{
type : "nav",
classes : [
"FeedLimitNav",
"FeedSelectDropdown"
],
children : [
{
type : "span",
classes : [
"FeedLimitCounter",
"FeedSelectLabel"
],
text : String( p.limit )
},
{
type : "ul",
classes : [
"FeedLimits",
"FeedSelectList"
],
children : LIMITS.map( u.createLimit )
}
]
},
{
type : "span",
classes : [
"FeedLimitDesc",
"FeedSelectDesc"
],
text : p.msg( "wa-limit-desc" ).escape( )
}
]
}
]
}
]
} );
u.header.append( u.rcButton, u.headerTabs, u.headerOptions );
};
u.createLimit = function( limit ) {
return {
type : "li",
classes : [
"FeedSelectListItem",
"FeedLimit"
],
text : String( limit ),
events : {
click : u.limitSelect
}
};
};
u.setType = function( t ) {
const types = Object.freeze( [
"main",
"watchlist",
"feeds",
"images"
] );
if ( types.includes( t ) ) {
u.el.dataset.type = t;
} else {
u.el.dataset.type = p.type;
}
u.type = u.el.dataset.type;
};
u.setTheme = function( h ) {
if ( arguments.length === 0 ) {
h = p.themeName;
}
const preservedClasses = Object.freeze( [
"Feed"
] );
u.el.classList.forEach( function( m ) {
if ( preservedClasses.includes( m ) ) return;
u.el.classList.remove( m );
} );
const l = "Feed__theme-" + h;
u.el.classList.add( l );
u.el.dataset.theme = h;
};
u.limitSelect = function( ev ) {
const l = ev.target.textContent;
const n = parseInt( l );
if ( isNaN( n ) || !isFinite( n ) ) return;
u.setLimit( n );
p.entries = [ ];
p.loaded = false;
u.addSpinner( );
p.load( n );
};
u.loadMore = function( ev ) {
ev.preventDefault( );
p.loadMore( );
};
u.setLimit = function( n ) {
const a = u.el.querySelector( ".FeedLimitCounter" );
a.textContent = n;
};
u.addHeaderTabs = function( tabs ) {
const t = tabs.map( u.generateTab );
u.tabs = createElement( "ul", {
id : "FeedTabs",
classes : [ "FeedTabs" ],
children : t
} );
if ( u._tabsAdded ) {
u.empty( u.headerTabs );
u.headerTabs.append( u.tabs );
} else {
u.headerTabs.append( u.tabs );
u._tabsAdded = true;
}
};
u.addModule = u.createModule = function( opts ) {
const classes = [ "FeedModule" ];
const title = [ ];
if ( opts.title ) {
classes.push( "has-title" );
title.push( {
type : "span",
classes : [ "FeedModuleTitle" ],
text : opts.title
} );
}
if ( opts.icon ) {
const icon = p.wds.icon( opts.icon );
classes.push( "has-icon" );
title.push( {
type : "div",
classes : [ "FeedModuleIcon" ],
children : [ icon ]
} );
if ( opts.title ) {
classes.push( "has-title" );
title.push( {
type : "span",
classes : [ "FeedModuleTitle" ],
} );
}
}
const heading = createElement( "h2", {
classes : [ "FeedModuleHeading" ],
children : title
} );
const obj = { };
if ( opts.classes ) {
if ( typeof opts.classes === "string" ) {
const clz = opts.classes.split( /\s+/g ).filter( function( m ) {
return m !== "";
} );
clz.forEach( function( cl ) {
if ( cl === "" ) return;
classes.push( cl );
} );
} else if ( Array.isArray( opts.classes ) ) {
opts.classes.forEach( function( cl ) {
classes.push( cl );
} );
}
}
if ( !opts.background ) {
classes.push( "no-background" );
}
obj.classes = classes;
if ( opts.id ) obj.id = opts.id;
const content = createElement( "div", {
classes : [ "FeedModuleContent" ],
condition : !!opts.content,
children : [ opts.content ]
} );
obj.children = [ ];
if ( opts.title || opts.icon ) {
obj.children.push( heading );
}
if ( opts.content ) {
obj.children.push( content );
}
const module = createElement( "section", obj );
u.rail.append( module );
};
u.generateTab = function( tab ) {
const tabPrefix = "wa-tab-";
const text = p.msg( tabPrefix + tab + "-text" ).plain( );
const page = p.msg( tabPrefix + tab + "-page" ).plain( );
const url = mw.util.getUrl( page );
const isCurrent = p.type === tab;
const classes = [ "FeedTab" ];
if ( isCurrent ) classes.push( "current" );
return {
type : "li",
classes : classes,
data : {
name : tab
},
children : [ {
type : "a",
classes : [ "FeedTabLink" ],
text : text,
attr : {
href : url
}
} ]
};
};
u.addEntries = function( entries ) {
if ( !u._entriesLoaded ) {
u.list = createElement( "ul", {
classes : [ "FeedEntries" ],
attr : {
id : "FeedEntries"
}
} );
u.more = createElement( "div", {
classes : [ "FeedMore" ],
attr : {
id : "FeedMore"
},
children : [
{
type : "a",
classes : [ "FeedMoreButton" ],
attr : {
id : "FeedMoreButton",
href : "#"
},
text : p.msg( "see-more-activity" ).plain( ),
events : {
click : u.loadMore
}
}
]
} );
u.entries = createElement( "nav", {
classes : [ "FeedEntriesWrapper" ],
attr : {
id : "FeedEntriesWrapper"
},
children : [
u.list,
u.more
]
} );
u.renderContent( u.entries );
u._entriesLoaded = true;
}
entries.forEach( u.renderEntry );
};
u.addFeedsEntries = function( entries ) {
if ( !u._entriesLoaded ) {
u.list = createElement( "ul", {
classes : [ "FeedPostEntries" ],
attr : {
id : "FeedPostEntries"
}
} );
u.entries = createElement( "nav", {
classes : [ "FeedPostEntriesWrapper" ],
attr : {
id : "FeedPostEntriesWrapper"
},
children : [
u.list
]
} );
u.renderContent( u.entries );
u._entriesLoaded = true;
}
entries.forEach( u.renderFeedsEntry );
};
u.renderContent = function( content ) {
if ( content instanceof Element ) {
u.empty( u.content );
u.content.append( content );
} else if ( typeof content === "string" ) {
u.content.innerHTML = content;
} else if ( isPlainObject( content ) ) {
createElement( u.content, content );
}
};
u.renderEntry = function( entry ) {
const params = Object.fromEntries( entry );
const el = createElement( "li", {
classes : [
"FeedEntry",
"feed-entry"
],
data : {
type : params.type,
ns : params.ns
},
children : u.renderChildren( params )
} );
u.list.append( el );
};
u.renderFeedsEntry = function( entry ) {
const params = Object.fromEntries( entry );
const el = createElement( "li", {
classes : [
"FeedPostEntry",
"feed-post-entry"
],
data : {
type : params.isReply ? "reply" : "post"
},
children : u.renderChildren( params )
} );
u.list.append( el );
};
u.renderChildren = function( params ) {
const type = p.type;
if ( p.customRendering.hasOwnProperty( type ) ) {
const f = p.customRendering[ type ];
return f.call( p, [ u, p ] );
}
switch ( type ) {
case "feeds" :
return u.renderFeeds( params );
case "images" :
return u.renderImages( params );
default :
return u.renderActivity( params );
}
};
u.renderActivity = function( params ) {
return [
// Creates an icon that corresponds to the entry
u.renderIcon,
// Creates a set of details for the following entry
u.renderDetails,
// Creates the actions button
u.renderActions
].map( function( f ) {
return f( params );
} );
};
u.renderImages = function( params ) {
return [
].map( function( f ) {
} );
};
u.getIcon = function( icon ) {
const hasIconName = ICON_NAMES.has( icon );
if ( !hasIconName ) {
return null;
}
return p.wds.icon( ICON_NAMES.get( icon ) );
};
u.renderIcon = function( params ) {
const icon = u.getIcon( params.icon );
return {
type : "div",
classes : [
"FeedIconContainer",
"feed-icon__container"
],
data : {
icon: params.icon
},
children : [ {
type : "span",
classes : [
"FeedIconWrapper",
"feed-icon__wrapper"
],
children : [ icon ]
} ],
condition : icon !== null
};
};
// Creating activity details
u.renderDetails = function( params ) {
return {
type : "section",
classes : [
"FeedDetails",
"feed-details"
],
children : [
u.createTitle,
u.createEdited,
u.createSummary,
u.createCategories,
u.createTemplates,
u.createImages
].map( function( f ) {
return f( params );
} )
};
};
// Page title
u.createTitle = function( params ) {
const url = mw.util.getUrl( params.title );
return {
type : "header",
classes : [
"FeedPageTitle",
"feed-title"
],
children : [ {
type : "a",
classes : [
"FeedPageTitleText",
"feed-title__text"
],
attr : {
href : url
},
text : params.title
} ]
};
};
// Edited user details
u.createEdited = function( params ) {
return {
type : "div",
classes : [
"FeedEdited",
"feed-edited"
],
children : [
{
type : "span",
classes : [
"FeedEditedText",
"feed-edited__text"
],
text : p.msg( params.editedKey ).escape( )
},
{
type : "a",
attr : {
href : mw.util.getUrl(
conf.wgFormattedNamespaces[ 2 ] +
":" + params.user
)
},
children : [
u.createAvatar,
u.createUserName
].map( function( f ) {
return f( params );
} )
},
// Creates a timeago element
u.renderTimeago( params ),
// Creates a diff button
u.renderDiff( params )
]
};
};
// User avatar
u.createAvatar = function( params ) {
return {
type : "div",
classes : [
"FeedEditedAvatar",
"feed-edited__avatar",
"wds-avatar"
],
children : [ {
type : "img",
classes : [
"FeedEditedAvatarImage",
"feed-edited__avatar-image",
"wds-avatar__image"
],
attr : {
src : params.avatar,
alt : params.user
}
} ]
};
};
u.createUserName = function( params ) {
return {
type : "em",
classes : [
"FeedEditedUsername",
"feed-edited__username"
],
text : params.user
};
};
// Activity summary
u.createSummary = function( params ) {
return {
type : "section",
classes : [
"FeedSection",
"feed-section",
"feed-section__summary"
],
data : {
type : "summary"
},
condition : params.summary !== "",
children : [
{
type : "div",
classes : [
"FeedSectionTitle",
"feed-section__title"
],
text : p.msg( params.summaryKey ).parse( )
},
{
type : "div",
classes : [
"FeedSectionField",
"feed-section__field"
],
html : params.summary
}
]
};
};
// Categories
u.createCategories = function( params ) {
const t = p.msg( "added-category", params.categories.length )
.parse( );
const c = params.categories.length !== 0;
return {
type : "section",
classes : [
"FeedSection",
"feed-section",
"feed-section__categories"
],
data : {
type : "categories"
},
condition : c,
children : [
{
type : "div",
classes : [
"FeedSectionTitle",
"feed-section__title"
],
text : t
},
{
type : "div",
classes : [
"FeedSectionField",
"feed-section__field"
],
children : params.categories.map(
u.createCategory
)
}
]
};
};
u.createCategory = function( category ) {
const url = mw.util.getUrl( category );
const pattern = new RegExp( "^" +
conf.wgFormattedNamespaces[ 14 ] +
":\\s*", "g" );
const title = category.replace( pattern, "" );
return {
type : "a",
classes : [
"FeedCategory",
"feed-category"
],
attr : {
href : url
},
text : title
};
};
// Categories
u.createTemplates = function( params ) {
const t = p.msg( "added-template", params.templates.length )
.parse( );
const exclude = params.ns === 2 && /\.js|\.css$/.test( params.title );
const c = params.templates.length !== 0 && !exclude;
return {
type : "section",
classes : [
"FeedSection",
"feed-section",
"feed-section__templates"
],
data : {
type : "templates"
},
condition : c,
children : [
{
type : "div",
classes : [
"FeedSectionTitle",
"feed-section__title"
],
text : t
},
{
type : "div",
classes : [
"FeedSectionField",
"feed-section__field"
],
children : params.templates.map(
u.createTemplate
)
}
]
};
};
u.createTemplate = function( title ) {
const page = conf.wgFormattedNamespaces[ 10 ] +
":" + title;
const url = mw.util.getUrl( page );
return {
type : "a",
classes : [
"FeedTemplate",
"feed-template"
],
attr : {
href : url
},
text : title
};
};
// Images
u.createImages = function( params ) {
const t = p.msg( "added-image", params.images.length )
.parse( );
const c = params.images.length !== 0;
return {
type : "section",
classes : [
"FeedSection",
"feed-section",
"feed-section__images"
],
data : {
type : "images"
},
condition : c,
children : [
{
type : "div",
classes : [
"FeedSectionTitle",
"feed-section__title"
],
text : t
},
{
type : "div",
classes : [
"FeedSectionField",
"feed-section__field"
],
children : params.images.map(
u.createImage
)
}
]
};
};
u.createImage = function( image ) {
const i = Object.fromEntries( image );
const src = i.src, url = i.url, title = i.title;
return {
type : "figure",
classes : [
"FeedImageWrapper",
"feed-image__wrapper"
],
children : [ {
type : "a",
classes : [
"FeedImageLink",
"feed-image__link"
],
/*events : {
click : function( event ) {
event.preventDefault( );
const target = event.target;
u.openLightbox( target );
}
},*/
attr : {
href : url
},
children : [
{
type : "img",
classes : [
"FeedImage",
"feed-image"
],
attr : {
src : src,
alt : title
},
data : {
"file-url" : url
}
},
{
type : "figcaption",
classes : [
"FeedImageFileName",
"feed-image__file-name"
],
text : title
}
]
} ]
};
};
u.renderTimeago = function( params ) {
return {
type : "div",
classes : [
"FeedTimeago",
"feed-timeago"
],
text : params.timeago
};
};
u.renderDiff = function( params ) {
const diffUrl = mw.util.getUrl( params.title, {
diff : params.revid,
oldid : params.oldid
} );
return {
type : "a",
condition : params.oldid !== 0,
classes : [
"FeedDiff",
"feed-history__button"
],
attr : {
href : diffUrl
},
children : [ u.getIcon( "diff" ) ]
};
};
u.renderActions = function( params ) {
return {
type : "div",
classes : [
"FeedActions",
"wds-dropdown"
],
children : [
{
type : "span",
children : [ u.getIcon( "more" ) ],
classes : [
"FeedActionsLabel",
"wds-dropdown__toggle"
]
},
{
type : "div",
classes : [
"FeedActionsContent",
"wds-dropdown__content",
"wds-is-not-scrollable"
],
children : [ {
type : "ul",
classes : [
"FeedActionsList",
"wds-list",
"wds-is-linked"
],
children : [
u.renderUndoButton/*,
u.renderRollbackButton,
u.renderBlockButton*/
].map( function( f ) {
return f( params );
} )
} ]
}
]
};
};
u.renderUndoButton = function( params ) {
const url = mw.util.getUrl( params.title, {
action : "edit",
undo : params.revid
} );
return {
type : "li",
data : {
action : "undo"
},
children : [
{
type : "a",
attr : {
href : url
},
text : p.msg( "wa-actions-undo" ).plain( )
}
]
};
};
u.renderRollbackButton = function( params ) {
return {
type : "li",
data : {
action : "rollback"
},
children : [
{
type : "a",
attr : {
href : url
},
text : p.msg( "wa-actions-rollback" ).plain( )
}
],
condition : p.canRollback
};
};
u.renderBlockButton = function( ) {
return {
type : "li",
data : {
action : "block"
},
children : [
{
type : "a",
attr : {
href : url
},
text : p.msg( "wa-actions-block" ).plain( )
}
],
condition : p.canBlock
};
};
u.renderDeleteButton = function( ) {
return {
type : "li",
data : {
action : "delete"
},
children : [
{
type : "a",
attr : {
href : url
},
text : p.msg( "wa-actions-delete" ).plain( )
}
],
condition : p.canRollback
};
};
/* Feeds */
u.renderFeeds = function( params ) {
return [
// Creates an icon that corresponds to the Feeds entry
u.renderFeedsIcon,
// Creates details about the Feeds entry
u.renderFeedsDetails
].map( function( f ) {
return f( params );
} );
};
u.renderFeedsIcon = function( params ) {
const icon = ( params.isReply ? "comment" : "add" );
return {
type : "div",
classes : [
"FeedIcon",
"feed-icon__container"
],
data : {
icon : icon
},
children : [ {
type : "span",
classes : [
"FeedIconWrapper",
"feed-icon__wrapper"
],
children : [ p.wds.icon( icon ) ]
} ]
};
};
u.renderFeedsDetails = function( params ) {
return {
type : "section",
classes : [
"FeedPostDetails",
"feed-post__details"
],
children : [
// Renders title for the Feeds post
u.createFeedsTitle,
// Renders Feeds content
u.createFeedsContent,
// Renders images
u.createFeedsImages,
// Renders tags
u.createFeedsTags,
// Renders embed
u.createFeedsEmbed
].map( function( f ) {
return f( params );
} )
};
};
u.createFeedsTitle = function( params ) {
const url = conf.wgServer + conf.wgScriptPath +
"/f/p/" + params.threadId;
const catUrl = new URL( conf.wgServer + conf.wgScriptPath +
"/f" );
catUrl.searchParams.append( "catId", params.forumId );
catUrl.searchParams.append( "sort", "latest" );
return {
type : "header",
classes : [
"FeedPostTitle",
"feed-post__title"
],
children : [ {
type : "div",
classes : [
"FeedPostTitleContent",
"feed-post__title-content"
],
children : [
{
type : "a",
classes : [
"FeedPostTitleText",
"feed-post__title-text"
],
attr : {
href : url
},
text : params.title
},
{
type : "span",
text : p.msg( "wa-feeds-in" ).plain( )
},
{
type : "a",
attr : {
href : String( catUrl )
},
text : params.forumName
}
]
} ]
};
};
u.createFeedsContent = function( params ) {
return {
type : "div",
classes : [
"FeedPostContentWrapper",
"feed-post__contentWrapper"
],
children : [
u.createFeedsAvatar,
u.createFeedsPostContent
].map( function( f ) {
return f( params );
} )
};
};
u.createFeedsAvatar = function( params ) {
return {
type : "div",
classes : [
"FeedPostAvatar",
"feed-post__avatar",
"wds-avatar"
],
children : [ {
type : "img",
classes : [
"FeedPostAvatarImage",
"feed-post__avatar-image",
"wds-avatar__image"
],
attr : {
src : params.avatar
}
} ]
};
};
u.createFeedsPostContent = function( params ) {
return {
type : "section",
classes : [
"FeedPostContent",
"feed-post__content"
],
children : [
{
type : "div",
classes : [
"FeedPostEditedBy",
"feed-post__edited-by"
],
children : u.createFeedsPostEdited( params )
},
{
type : "div",
classes : [
"FeedPostText",
"feed-post__text"
],
children : [
u.createFeedsPostText
].map( function( f ) {
return f( params );
} )
}
]
};
};
u.createFeedsPostEdited = function( params ) {
const w = p.wallsEnabled ? "wall" : "talk";
const n = p.wallsEnabled ? 1200 : 3;
const wallTitle = p.msg( "wa-feeds-post-" + w ).plain( );
const ns = conf.wgFormattedNamespaces[ n ];
const page = ns + ":" + params.usernameEncoded;
const url = mw.util.getUrl( page );
const contribsTitle = p.msg( "wa-feeds-post-contribs" ).plain( );
const cns = conf.wgFormattedNamespaces[ -1 ];
const cpage = cns + ":" + contribsTitle + "/" + params.usernameEncoded;
const cUrl = mw.util.getUrl( cpage );
const editedKey = params.isReply ? "commented" : "created";
const fUrl = conf.wgServer + conf.wgScriptPath + "/f/u/" +
params.userId;
const blockTitle = p.msg( "wa-feeds-post-block" ).plain( );
const blockpage = cns + ":" + blockTitle + "/" + params.usernameEncoded;
const bUrl = mw.util.getUrl( blockpage );
return [
{
type : "span",
classes : [
"FeedPostEditedText",
"feed-post__edited-text"
],
text : p.msg( "wa-feeds-post-" + editedKey + "-key" )
.plain( )
},
{
type : "a",
classes : [
"FeedPostEditedUser",
"feed-post__edited-user"
],
attr : {
href : fUrl
},
text : params.userName
},
{
type : "span",
classes : [
"FeedPostEditedLinks",
"feed-post__edited-links"
],
children : [
"(",
{
type : "a",
attr : {
href : url
},
text : wallTitle
},
"|",
{
type : "a",
attr : {
href : cUrl
},
text : contribsTitle
},
{
type : "#text",
text : "|",
condition : p.canBlock
},
{
type : "a",
attr : {
href : bUrl
},
text : blockTitle,
condition : p.canBlock
},
")"
]
}
];
};
u.createFeedsPostText = function( params ) {
return {
type : "div",
classes : [
"FeedPostTextContent",
"feed-post__text-content"
],
text : params.text
};
};
u.createFeedsImages = function( params ) {
return {
type : "div"
};
};
u.createFeedsTags = function( params ) {
return {
type : "div"
};
};
u.createFeedsEmbed = function( params ) {
return {
type : "div"
};
};
u.appendTo = function( el ) {
if ( !( el instanceof Element ) ) return;
el.append( u.el );
};
u.init( );
}
mw.hook( "dev.i18n" ).add( function( i18no ) {
Promise.all( [
i18no.loadMessages( NAME, { noCache : true } ),
mw.loader.using( deps )
] ).then( function( m ) {
const i18n = m[ 0 ];
window.WikiActivity =
window.UCP.WikiActivity =
new WikiActivity( options, i18n );
window.UCP.WikiActivityController = WikiActivity;
window.UCP.WikiActivityUI = FeedUI;
mw.hook( "ucp.wikiactivity" ).fire( window.UCP.WikiActivity );
} );
} );
} )( this, jQuery, mediaWiki );