User:Guycn2/UserInfoPopup.js

/*== User Info Popup ==Adds an "i" (info) icon at the top of user-related pages(e.g. user pages, user talk pages, "Contributions" pages, etc.)The color of the "i" icon represents the amount of time passed since the user last edited:* Green – user last edited less than 20 minutes ago* Orange – user last edited more than 20 minutes ago, but less than 3 months ago* Red – user last edited more than 3 months agoHover over the "i" icon to quickly view useful information about the relevant user:* Registration date* Number of edits* Time elapsed since last edit* User groups (rights), incl. global ones* Latest block time (incl. range and global blocks, when applicable)* Gender (if disclosed)See full documentation at:[[User:Guycn2/UserInfoPopup]]See also:* [[User:Guycn2/UserInfoPopup.css]] – for the corresponding style sheetSkins supported:Vector (both 2022 and 2010), Monobook, Timeless, and Minerva.Also fully supported on the mobile interface.Dependencies:* mediawiki.api* mediawiki.language.months* mediawiki.user* mediawiki.util* user.options* oojs-ui-coreWritten by: [[User:Guycn2]]*/( async () => {'use strict';const username = mw.config.get( 'wgRelevantUserName' );if ( !username || mw.config.get( 'userInfoPopupLoaded' ) ) {return;}mw.config.set( 'userInfoPopupLoaded', true );await mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] );const isAnon = mw.util.isIPAddress( username );const api = new mw.Api();async function checkIfUserExists() {if ( isAnon ) {return true;}const data = await api.get( { list: 'users', ususers: username } );if ( data.query.users[ 0 ].userid ) {return true;}return false;}if ( !( await checkIfUserExists() ) ) {return;}mw.loader.load('https:https://www.search.com.vn/wiki/index.php?lang=en&q=User:Guycn2/UserInfoPopup.css&action=raw&ctype=text/css','text/css');const scriptData = {lang: mw.config.get( 'wgUserLanguage' ),skin: mw.config.get( 'skin' ),secsFromLastEdit: await calcSecsFromLastEdit()};createInfoIcon();await $.when( mw.loader.using( 'oojs-ui-core' ), $.ready );addInfoIconToPage();attachEventListeners();function i18n( key ) {const messages = {en: {infoIconAlt: 'Info icon',femaleSymbolAlt: 'Female',maleSymbolAlt: 'Male',fetchingData: 'Fetching data…',regUnknown: 'Unknown',joined: 'Joined:',editCount: 'Edits:',lastEdited: 'Last edited:',lastEditedNever: 'Never',lastEditedUnknown: 'Unknown',groups: 'Groups:',noGroups: 'None',lastBlocked: 'Last blocked:',neverBlocked: 'Never',partiallyBlocked: 'Currently blocked (partially)',fullyBlocked: 'Currently blocked',rangeBlockedPartially: 'Currently range-blocked (partially)',rangeBlockedFully: 'Currently range-blocked',globallyBlocked: 'Currently blocked globally',globallyLocked: 'Currently locked globally',ago: '$1 ago',seconds: [ '1 second', '$1 seconds' ],minutes: [ '1 minute', '$1 minutes' ],hours: [ '1 hour', '$1 hours' ],days: [ '1 day', '$1 days' ],weeks: [ '1 week', '$1 weeks' ],months: [ '1 month', '$1 months' ],years: [ '1 year', '$1 years' ]},he: {infoIconAlt: 'צלמית מידע',femaleSymbolAlt: 'נקבה',maleSymbolAlt: 'זכר',fetchingData: 'המידע בטעינה…',regUnknown: 'לא ידוע',joined: 'הרשמה:',editCount: 'עריכות:',lastEdited: 'עריכה אחרונה:',lastEditedNever: 'אין',lastEditedUnknown: 'לא ידוע',groups: 'קבוצות:',noGroups: 'ללא',lastBlocked: 'חסימה אחרונה:',neverBlocked: 'אין',partiallyBlocked: 'חסימה פעילה כעת (חלקית)',fullyBlocked: 'חסימה פעילה כעת',rangeBlockedPartially: 'חסימת טווח פעילה כעת (חלקית)',rangeBlockedFully: 'חסימת טווח פעילה כעת',globallyBlocked: 'חסימה גלובלית פעילה כעת',globallyLocked: 'נעילה גלובלית פעילה כעת',ago: 'לפני $1',seconds: [ 'שנייה', '$1 שניות' ],minutes: [ 'דקה', '$1 דקות' ],hours: [ 'שעה', 'שעתיים', '$1 שעות' ],days: [ 'יום', 'יומיים', '$1 ימים' ],weeks: [ 'שבוע', 'שבועיים', '$1 שבועות' ],months: [ 'חודש', 'חודשיים', '$1 חודשים' ],years: [ 'שנה', 'שנתיים', '$1 שנים' ]}};if (messages[ scriptData.lang ] &&messages[ scriptData.lang ][ key ]) {return messages[ scriptData.lang ][ key ];} else {return messages.en[ key ];}}async function calcSecsFromLastEdit() {const params = {list: 'usercontribs',ucuser: username,ucprop: 'timestamp',uclimit: 1};const data = await api.get( params );if ( data.query.usercontribs.length === 0 ) {return null;}const lastEditTime =new Date( data.query.usercontribs[ 0 ].timestamp ).getTime();return ( mw.now() - lastEditTime ) / 1000;}function createInfoIcon() {const $img = $( '<img>' ).addClass( 'user-info-popup-icon' ).attr( {alt: i18n( 'infoIconAlt' ),width: '20.3',height: '20.3'} );if ( scriptData.secsFromLastEdit === null ) {$img.addClass( 'user-info-popup-grey-icon' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/d/df/Information_grey.svg' );} else if ( scriptData.secsFromLastEdit < 60 * 20 ) {$img.addClass( 'user-info-popup-green-icon' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/7/7d/Information_green.svg' );} else if ( scriptData.secsFromLastEdit < 60 * 60 * 24 * 30 * 3 ) {$img.addClass( 'user-info-popup-orange-icon' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/f/f0/Information_orange.svg' );} else {$img.addClass( 'user-info-popup-red-icon' ).attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/5/55/Information_red.svg' );}scriptData.$indicator = $( '<div>' ).addClass( 'mw-indicator' ).attr( { id: 'mw-indicator-user-info-popup-indicator', tabindex: '0' } ).append( $img );}function addInfoIconToPage() {const $throbberImg = $( '<img>' ).attr( {alt: i18n( 'fetchingData' ),id: 'user-info-popup-throbber',src: 'https://upload.wikimedia.org/wikipedia/commons/f/f8/Ajax-loader(2).gif'} );const $placeholderText = $( '<p>' ).attr( 'id', 'user-info-popup-placeholder-text' ).text( i18n( 'fetchingData' ) );scriptData.$popupPlaceholder = $( '<div>' ).attr( 'id', 'user-info-popup-placeholder' ).append( $throbberImg, $placeholderText );scriptData.popup = new OO.ui.PopupWidget( {$content: scriptData.$popupPlaceholder,align: 'backwards',autoFlip: false,id: 'user-info-popup-popup',hideWhenOutOfView: false,padded: true,position: 'below',width: 225} );scriptData.$indicator.append( scriptData.popup.$element );if (scriptData.skin === 'vector-2022' &&$( '.vector-page-toolbar-container:has( #ca-nstab-user )' ).length) {scriptData.$indicator.insertBefore( '.vector-page-tools-landmark:has( #vector-page-tools-dropdown )' );} else {const $indicatorsContainer = $( '.mw-indicators' );if (!window.matchMedia( '( orientation: portrait )' ).matches ||scriptData.skin === 'vector-2022' ||scriptData.skin === 'vector' ||( scriptData.skin === 'monobook' && !$( '#sidebar-toggle:visible' ).length )) {scriptData.popup.setAlignment( 'forwards' );scriptData.popup.setPosition( 'before' );if ( $indicatorsContainer.children( '.mw-indicator' ).length >= 6 ) {scriptData.popup.setAutoFlip( true );}}if ( scriptData.skin === 'minerva' ) {scriptData.$indicator.css( 'float', $( 'body.rtl' ).length ? 'left' : 'right' ).appendTo( '.header-container' );} else {$indicatorsContainer.prepend( scriptData.$indicator );}}}function attachEventListeners() {scriptData.popup.on( 'ready', () => {// Prevent mobile browsers from occasionally jumping// to the top of the page when tapping the "i" icon.window.scrollTo( scriptData.posX, scriptData.posY );if (document.documentElement.clientWidth < 600 &&scriptData.skin === 'vector-2022' &&scriptData.popup.$element.hasClass( 'oo-ui-popupWidget-anchored-top' )) {adaptPopupPosition();}scriptData.popup.$element.hide().fadeIn();} );scriptData.$indicator.on( 'mouseenter focusin keydown', e => {if ( e.type === 'keydown' ) {if ( ![ 'Enter', ' ' ].includes( e.key ) ) {return;}if ( e.key === ' ' ) {e.preventDefault();}}clearTimeout( scriptData.mouseLeaveTimeout );scriptData.mouseEnterTimeout = setTimeout( openPopup, 200 );} );scriptData.$indicator.on( 'mouseleave focusout', () => {if (document.activeElement.id === 'mw-indicator-user-info-popup-indicator' ||document.activeElement.parentElement.classList.contains('user-info-popup-value')) {return;}clearTimeout( scriptData.mouseEnterTimeout );scriptData.mouseLeaveTimeout = setTimeout( closePopup, 2500 );} );$( document ).on( 'keydown', e => {if ( e.key === 'Escape' ) {closePopup();}} );$( document ).on( 'click', closePopup );$( '.oo-ui-fieldsetLayout-header, .ext-discussiontools-init-section-bar' ).on( 'click', closePopup );scriptData.$indicator.on( 'click', e => e.stopPropagation() );}function adaptPopupPosition() {const innerBody = document.querySelector( '.mw-page-container' );const innerBodyRect = innerBody.getBoundingClientRect();const indicator = scriptData.$indicator[ 0 ];const indicatorRect = indicator.getBoundingClientRect();const dir = $( 'body.rtl' ).length ? 'left' : 'right';const pos =Math.abs( indicatorRect[ dir ] - innerBodyRect[ dir ] ) -indicator.offsetWidth / 2;scriptData.popupCss = mw.util.addCSS(`#user-info-popup-popup { ${ dir }: ${ pos }px !important; }`);}function openPopup() {if ( !scriptData.popup.isVisible() ) {// posX and posY are used to prevent mobile browsers from // occasionally jumping to the top of the page when tapping// the "i" icon. See the popup's "ready" event listener above.scriptData.posX = window.scrollX;scriptData.posY = window.scrollY;scriptData.popup.toggle( true );if ( !scriptData.dataFetched ) {getUserData().then( fillPopupContent );scriptData.dataFetched = true;}}}function closePopup() {clearTimeout( scriptData.mouseLeaveTimeout );if ( scriptData.popup.isVisible() ) {scriptData.popup.$element.fadeOut( () => {scriptData.popup.toggle( false );scriptData.popup.$element.show();if ( scriptData.popupCss ) {scriptData.popupCss.disabled = true;}} );}}async function getUserData() {let params;if ( isAnon ) {params = {list: 'blocks|globalblocks|logevents|usercontribs',bkip: username,bkprop: 'flags|user',bklimit: 2,bgip: username,bgprop: 'address',bglimit: 1,leaction: 'block/block',letitle: `User:${ username }`,leprop: 'timestamp',lelimit: 1,ucuser: username,ucprop: '',uclimit: 'max'};} else {params = {list: 'blocks|logevents|usercontribs|users',meta: 'globaluserinfo',bkusers: username,bkprop: 'flags',bklimit: 1,leaction: 'block/block',letitle: `User:${ username }`,leprop: 'timestamp',lelimit: 1,ucuser: username,ucdir: 'newer',ucprop: 'timestamp',uclimit: 1,ususers: username,usprop: 'editcount|gender|groupmemberships|registration',guiuser: username,guiprop: 'groups'};}const data = await api.get( params );if ( isAnon ) {const editCount = data.query.usercontribs.length;scriptData.editCount = await renderAnonEditCount( editCount );scriptData.isGloballyBlocked = data.query.globalblocks.length;if ( scriptData.isGloballyBlocked ) {scriptData.globalBlockTarget = data.query.globalblocks[ 0 ].address;}} else {scriptData.gender = data.query.users[ 0 ].gender;if ( data.query.users[ 0 ].registration ) {scriptData.regDate =await formatDate( data.query.users[ 0 ].registration, true );} else if ( data.query.usercontribs[ 0 ] ) {scriptData.regDate =await formatDate( data.query.usercontribs[ 0 ].timestamp, true );} else {scriptData.regDate = i18n( 'regUnknown' );}scriptData.editCount = data.query.users[ 0 ].editcount.toLocaleString();const localGroups =data.query.users[ 0 ].groupmemberships.map( item => item.group );scriptData.localGroups = await renderGroups( localGroups );if ( data.query.globaluserinfo.groups ) {const globalGroups = data.query.globaluserinfo.groups.filter(item => !localGroups.includes( item ));scriptData.globalGroups = await renderGroups( globalGroups );scriptData.isLocked = data.query.globaluserinfo.locked === '';}}const blocks = data.query.blocks;scriptData.isBlocked = blocks.length;if ( scriptData.isBlocked ) {if ( isAnon && blocks[ 0 ].user !== username && blocks[ 1 ] ) {blocks.shift();}scriptData.isPartiallyBlocked = blocks[ 0 ].partial === '';scriptData.isRangeBlocked = isAnon && blocks[ 0 ].user !== username;if ( scriptData.isRangeBlocked ) {scriptData.rangeBlockTarget = blocks[ 0 ].user;}} else if ( data.query.logevents.length ) {scriptData.lastBlockDate =await formatDate( data.query.logevents[ 0 ].timestamp, false );}}async function renderAnonEditCount( editCount ) {if ( editCount < 500 ) {return editCount.toLocaleString();}await mw.loader.using( 'mediawiki.user' );const rights = await mw.user.getRights();const maxAnonEditCount = rights.includes( 'apihighlimits' ) ? 5000 : 500;if ( editCount === maxAnonEditCount ) {return `${ editCount.toLocaleString() }+`;} else {return editCount.toLocaleString();}}async function renderGroups( groups ) {if ( groups.length === 0 ) {return '';}let sysMsgGroups = '';groups.forEach( ( group, index ) => {sysMsgGroups += `{${ '{' }int:group-${ group }}}`;if ( index < groups.length - 1 ) {sysMsgGroups += ', ';}} );const params = {action: 'parse',uselang: scriptData.lang,text: sysMsgGroups,prop: 'text',contentmodel: 'wikitext',disablelimitreport: true};const data = await api.get( params );return $( data.parse.text[ '*' ] ).find( 'p' ).text().trim();}async function formatDate( timestamp, includeDay ) {await mw.loader.using( 'mediawiki.language.months' );const date = new Date( timestamp );const monthName = mw.language.months.names[ date.getMonth() ];const monthNameGen = mw.language.months.genitive[ date.getMonth() ];const year = date.getFullYear();if ( includeDay ) {const day = date.getDate();await mw.loader.using( 'user.options' );if ( mw.user.options.get( 'date' ) === 'mdy' ) {return `${ monthName } ${ day }, ${ year }`;} else {return `${ day } ${ monthNameGen } ${ year }`;}} else {return `${ monthName } ${ year }`;}}function fillPopupContent() {const $container = $( '<aside>' ).attr( 'id', 'user-info-popup-content' );const $header = $( '<header>' ).attr( 'id', 'user-info-popup-header' );$header.append($( '<bdi>' ).attr( 'id', 'user-info-popup-username' ).text( mw.util.prettifyIP( username ) ));const $ul = $( '<ul>' ).attr( 'id', 'user-info-popup-list' );$container.append( $header, $ul );if ( !isAnon ) {addListItem( $ul, i18n( 'joined' ), scriptData.regDate );}const editCounterUrl =`https://xtools.wmcloud.org/ec/${ mw.config.get( 'wgServerName' ) }/${ encodeURIComponent( username ) }`;addListItem($ul,i18n( 'editCount' ),`<a target="_blank" href="${ editCounterUrl }">${ scriptData.editCount }</a>`);const contribsUrl = mw.util.getUrl( `Special:Contributions/${ username }` );let lastEditedText;if ( scriptData.editCount === ( 0 ).toLocaleString() ) {lastEditedText = i18n( 'lastEditedNever' );} else if ( scriptData.secsFromLastEdit === null ) {lastEditedText = i18n( 'lastEditedUnknown' );} else {lastEditedText = i18n( 'ago' ).replace( '$1', calcTimeFromLastEdit() );}addListItem($ul,i18n( 'lastEdited' ),`<a href="${ contribsUrl }">${ lastEditedText }</a>`);if ( !isAnon ) {const localGroupsUrl = mw.util.getUrl( `Special:UserRights/${ username }` );const globalGroupsUrl =mw.util.getUrl( `m:Special:GlobalUserRights/${ username }` );let groupsHtml;if ( !scriptData.localGroups && !scriptData.globalGroups ) {groupsHtml =`<a href="${ localGroupsUrl }">${ i18n( 'noGroups' ) }</a>`;}if ( scriptData.localGroups && !scriptData.globalGroups ) {groupsHtml =`<a href="${ localGroupsUrl }">${ scriptData.localGroups }</a>`;}if ( !scriptData.localGroups && scriptData.globalGroups ) {groupsHtml =`<a href="${ globalGroupsUrl }">${ scriptData.globalGroups }</a>`;}if ( scriptData.localGroups && scriptData.globalGroups ) {groupsHtml =`<a href="${ localGroupsUrl }">${ scriptData.localGroups }</a>,<a href="${ globalGroupsUrl }">${ scriptData.globalGroups }</a>`;}addListItem( $ul, i18n( 'groups' ), groupsHtml );}let lastBlockText;let blockLogUrl = mw.util.getUrl( 'Special:Log', {type: 'block',page: `User:${ username }`} );if ( scriptData.isGloballyBlocked ) {lastBlockText = i18n( 'globallyBlocked' );blockLogUrl = mw.util.getUrl( 'm:Special:Log', {type: 'gblblock',page: `User:${ scriptData.globalBlockTarget }`} );} else if ( scriptData.isLocked ) {lastBlockText = i18n( 'globallyLocked' );blockLogUrl = mw.util.getUrl( 'm:Special:Log', {type: 'globalauth',page: `User:${ username }@global`} );} else if ( scriptData.isBlocked ) {if ( scriptData.isRangeBlocked ) {if ( scriptData.isPartiallyBlocked ) {lastBlockText = i18n( 'rangeBlockedPartially' );} else {lastBlockText = i18n( 'rangeBlockedFully' );}blockLogUrl = mw.util.getUrl( 'Special:Log', {type: 'block',page: `User:${ scriptData.rangeBlockTarget }`} );} else {if ( scriptData.isPartiallyBlocked ) {lastBlockText = i18n( 'partiallyBlocked' );} else {lastBlockText = i18n( 'fullyBlocked' );}}} else {lastBlockText = scriptData.lastBlockDate || i18n( 'neverBlocked' );}addListItem($ul,i18n( 'lastBlocked' ),`<a href="${ blockLogUrl }">${ lastBlockText }</a>`);if ( !isAnon && scriptData.gender !== 'unknown' ) {const images = {female: {alt: i18n( 'femaleSymbolAlt' ),path: 'https://upload.wikimedia.org/wikipedia/commons/8/8a/Light_pink_Venus_symbol.svg'},male: {alt: i18n( 'maleSymbolAlt' ),path: 'https://upload.wikimedia.org/wikipedia/commons/2/29/Light_blue_Mars_symbol.svg'}};$( '<img>' ).attr( {alt: images[ scriptData.gender ].alt,id: 'user-info-popup-gender-symbol',src: images[ scriptData.gender ].path,width: '16.6',height: '16.6'} ).appendTo( $header );}scriptData.$popupPlaceholder.replaceWith( $container );}function addListItem( $ul, property, value ) {const $li = $( '<li>' );const $property = $( '<span>' ).addClass( 'user-info-popup-property' ).text( property );const $value = $( '<span>' ).addClass( 'user-info-popup-value' ).html( value );$li.append( $property, ' ', $value ).appendTo( $ul );}function calcTimeFromLastEdit() {const secs = scriptData.secsFromLastEdit;const days = secs / 60 / 60 / 24;if ( secs < 60 ) {let fullSecs = Math.floor( secs );if ( fullSecs < 1 ) {fullSecs = 1;}const secsArrLength = i18n( 'seconds' ).length;if ( fullSecs < secsArrLength ) {return i18n( 'seconds' )[ fullSecs - 1 ];} else {return i18n( 'seconds' )[ secsArrLength - 1 ].replace( '$1', fullSecs );}} else if ( secs < 60 * 60 ) {const fullMins = Math.floor( secs / 60 );const minsArrLength = i18n( 'minutes' ).length;if ( fullMins < minsArrLength ) {return i18n( 'minutes' )[ fullMins - 1 ];} else {return i18n( 'minutes' )[ minsArrLength - 1 ].replace( '$1', fullMins );}} else if ( secs < 60 * 60 * 24 ) {const fullHours = Math.floor( secs / 60 / 60 );const hoursArrLength = i18n( 'hours' ).length;if ( fullHours < hoursArrLength ) {return i18n( 'hours' )[ fullHours - 1 ];} else {return i18n( 'hours' )[ hoursArrLength - 1 ].replace( '$1', fullHours );}} else if ( days < 7 ) {const fullDays = Math.floor( days );const daysArrLength = i18n( 'days' ).length;if ( fullDays < daysArrLength ) {return i18n( 'days' )[ fullDays - 1 ];} else {return i18n( 'days' )[ daysArrLength - 1 ].replace( '$1', fullDays );}} else if ( days < 30 ) {const fullWeeks = Math.floor( days / 7 );const weeksArrLength = i18n( 'weeks' ).length;if ( fullWeeks < weeksArrLength ) {return i18n( 'weeks' )[ fullWeeks - 1 ];} else {return i18n( 'weeks' )[ weeksArrLength - 1 ].replace( '$1', fullWeeks );}} else if ( days < 365 ) {let fullMonths = Math.floor( days / 30 );if ( fullMonths === 12 ) {fullMonths = 11;}const monthsArrLength = i18n( 'months' ).length;if ( fullMonths < monthsArrLength ) {return i18n( 'months' )[ fullMonths - 1 ];} else {return i18n( 'months' )[ monthsArrLength - 1 ].replace( '$1', fullMonths );}} else {const fullYears = Math.floor( days / 365 );const yearsArrLength = i18n( 'years' ).length;if ( fullYears < yearsArrLength ) {return i18n( 'years' )[ fullYears - 1 ];} else {return i18n( 'years' )[ yearsArrLength - 1 ].replace( '$1', fullYears );}}}} )();