Jump to content

User:Dr vulpes/Scripts/UserHoverStats.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
( function () {
	/**
	 * Little popup script showing user stats on hover. I'm still learning Javascript so if things are silly or hacky ¯\_(ツ)_/¯
	 */
	class UserHoverStats {
		constructor( $, mw, window ) {
			this.$ = $;
			this.mw = mw;
			this.window = window;

			this.userInfoCache = {};
			this.$currentPopup = null;
		}

		async execute() {
			const $links = this.$( '#mw-content-text a, #bodyContent a' );
			$links.each( ( _, link ) => {
				const $link = this.$( link );
				if ( this.isUserPageLink( $link ) ) {
					this.bindHoverEvent( $link );
				}
			} );
		}

		isUserPageLink( $link ) {
			// Basic checks: must have href, must link to User: or User_talk:
			const href = $link.attr( 'href' );
			if ( !href ) return false;
			const isUser = href.includes( '/wiki/User:' ) || href.includes( '?title=User:' );
			const isUserTalk = href.includes( '/wiki/User_talk:' ) || href.includes( '?title=User_talk:' );
			return ( isUser || isUserTalk );
		}

		bindHoverEvent( $link ) {
			$link.on( 'mouseenter.UserHoverStats', async ( e ) => {
				// Get the username by from the link
				const userName = this.extractUserFromLink( $link );
				if ( !userName ) {
					return;
				}

				// Get user data (editcount, block status, etc.)
				let userData;
				try {
					userData = await this.getUserStats( userName );
				} catch ( err ) {
					return;
				}

				// Show near hovered link
				this.showPopup( $link, userData );
			} );

			// On mouseleave: hide any visible popup
			$link.on( 'mouseleave.UserHoverStats', () => {
				this.hidePopup();
			} );
		}

		extractUserFromLink( $link ) {
			try {
				// Normalize from URL
				const href = $link.attr( 'href' );
				const urlObj = new this.mw.Uri( href, { strictMode: false } );
				let title = this.mw.util.getParamValue( 'title', href );
				if ( !title && urlObj.path.startsWith( '/wiki/' ) ) {
					title = decodeURIComponent( urlObj.path.replace( /^\/wiki\//, '' ) );
				}
				if ( !title ) return null;

				const titleObj = new this.mw.Title( title );
				if ( ![2,3].includes( titleObj.getNamespaceId() ) ) {
					return null;
				}
				return titleObj.getMainText();  // The username portion
			} catch {
				return null;
			}
		}

		async getUserStats( userName ) {
			// Return cached if available
			if ( this.userInfoCache[ userName ] ) {
				return this.userInfoCache[ userName ];
			}

			const userData = {
				userName,
				editCount: 0,
				articlesCreated: 0,
				blocked: false,
				blockReason: ''
			};

			// 1) Fetch user info: editcount, blockinfo
			let userResponse = await new this.mw.Api().get( {
				action: 'query',
				list: 'users',
				ususers: userName,
				usprop: 'blockinfo|editcount|registration',
				format: 'json'
			} );
			const userObj = ( userResponse?.query?.users && userResponse.query.users[0] ) || null;
			if ( userObj ) {
				userData.editCount = userObj.editcount || 0;
				if ( userObj.blockid ) {
					userData.blocked = true;
					userData.blockReason = userObj.blockreason || '';
				}
			}

			// Approximate “articles created” by looking at new contributions in namespace 0
			// One request, no continuation. Adjust this as needed.
			//  I am not done with this yet so if there are problems that's my bad
			let contribResponse = await new this.mw.Api().get( {
				action: 'query',
				list: 'usercontribs',
				ucuser: userName,
				ucshow: 'new',       // only show newly created pages
				ucnamespace: 0,      // article namespace only
				uclimit: 50000,         // change this limit if needed I don't have a better way to do this right now
				format: 'json'
			} );
			const contribs = contribResponse?.query?.usercontribs || [];
			userData.articlesCreated = contribs.length;

			// Cache the result
			this.userInfoCache[ userName ] = userData;
			return userData;
		}

		showPopup( $link, userData ) {
			// Remove any existing popup
			this.hidePopup();

			const $popup = this.$( '<div>' )
				.addClass( 'userHoverPopup' )
				.css( {
					position: 'absolute',
					zIndex: 9999,
					background: '#fff',
					border: '1px solid #444',
					padding: '5px',
					maxWidth: '250px',
					fontSize: '90%'
				} );

			// Bring the message together 
			let contentHtml = `
				<b>${this.$.escapeSelector( userData.userName )}</b><br>
				Edits: ${userData.editCount}<br>
				Articles created: ${userData.articlesCreated}<br>
			`;
			if ( userData.blocked ) {
				contentHtml += `<span style="color:red;">Blocked</span><br>
					Reason: ${this.$.escapeSelector( userData.blockReason )}<br>`;
			} else {
				contentHtml += 'Not currently blocked<br>';
			}

			$popup.html( contentHtml );

			const offset = $link.offset();
			$popup.css( {
				top: offset.top + $link.height() + 5,
				left: offset.left
			} );

			this.$( 'body' ).append( $popup );
			this.$currentPopup = $popup;
		}

		hidePopup() {
			if ( this.$currentPopup ) {
				this.$currentPopup.remove();
				this.$currentPopup = null;
			}
		}
	}

	mw.hook( 'wikipage.content' ).add( async () => {
		await mw.loader.using( [
			'mediawiki.util',
			'mediawiki.Uri',
			'mediawiki.Title',
			'mediawiki.api'
		], () => {
			( new UserHoverStats( window.jQuery, mw, window ) ).execute();
		} );
	} );

	mw.hook( 'postEdit' ).add( async () => {
		await mw.loader.using( [
			'mediawiki.util',
			'mediawiki.Uri',
			'mediawiki.Title',
			'mediawiki.api'
		], () => {
			( new UserHoverStats( window.jQuery, mw, window ) ).execute();
		} );
	} );
}() );
// </nowiki>