Jump to content

User:Guycn2/UserInfoPopup.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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.
/*


== 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 ago

Hover 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 sheet

Skins 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-core


Written 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://en.wikipedia.org/w/index.php?title=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', '/media/wikipedia/commons/d/df/Information_grey.svg' );
			
		} else if ( scriptData.secsFromLastEdit < 60 * 20 ) {
			
			$img
			.addClass( 'user-info-popup-green-icon' )
			.attr( 'src', '/media/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', '/media/wikipedia/commons/f/f0/Information_orange.svg' );
			
		} else {
			
			$img
			.addClass( 'user-info-popup-red-icon' )
			.attr( 'src', '/media/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: '/media/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
		) {
			
			const $navBtns = $( '#right-navigation > nav:first-of-type' );
			
			if ( $navBtns.length ) {
				scriptData.$indicator.insertAfter( $navBtns );
			} else {
				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: '/media/wikipedia/commons/archive/1/1d/20240712201036!Venus_symbol_(light_pink).svg'
				},
				
				male: {
					alt: i18n( 'maleSymbolAlt' ),
					path: '/media/wikipedia/commons/archive/b/b4/20240624040032!Mars_symbol_(bold_light_blue).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 );
				
			}
			
		}
		
	}
	
} )();