// ==UserScript==
// @name Mod Flagger Stats
// @description Post hover in mod flag queue, get and display flaggers stats. Badge links to user's flag history. Non-mods only can view their own flag badge on profile.
// @homepage https://github.com/samliew/SO-mod-userscripts
// @author Samuel Liew
// @version 5.0.13
//
// @match https://*.stackoverflow.com/users/*
// @match https://*.serverfault.com/users/*
// @match https://*.superuser.com/users/*
// @match https://*.askubuntu.com/users/*
// @match https://*.mathoverflow.net/users/*
// @match https://*.stackapps.com/users/*
// @match https://*.stackexchange.com/users/*
// @match https://stackoverflowteams.com/c/*/users/*
//
// @match https://*.stackoverflow.com/admin/dashboard*
// @match https://*.serverfault.com/admin/dashboard*
// @match https://*.superuser.com/admin/dashboard*
// @match https://*.askubuntu.com/admin/dashboard*
// @match https://*.mathoverflow.net/admin/dashboard*
// @match https://*.stackapps.com/admin/dashboard*
// @match https://*.stackexchange.com/admin/dashboard*
// @match https://stackoverflowteams.com/c/*/admin/dashboard*
//
// @exclude https://api.stackexchange.com/*
// @exclude https://data.stackexchange.com/*
// @exclude https://contests.stackoverflow.com/*
// @exclude https://winterbash*.stackexchange.com/*
// @exclude *chat.*
// @exclude *blog.*
// @exclude */tour
//
// @require https://raw.githubusercontent.com/samliew/SO-mod-userscripts/master/lib/se-ajax-common.js
// @require https://raw.githubusercontent.com/samliew/SO-mod-userscripts/master/lib/common.js
// @require https://raw.githubusercontent.com/samliew/ajax-progress/master/jquery.ajaxProgress.js
// ==/UserScript==
/* globals StackExchange, store, _window */
///
'use strict';
const superusers = [584192];
const isSuperuser = superusers.includes(selfId);
const isModPage = () => document.body.classList.contains('mod-page');
const repStrToNumeric = v => v.replace(/k/, '000').replace(/[^\d]/g, '') * 1 || 0;
function calculateFlagTier(fTotal = 0, fPerc = 0) {
// Default
let v = { tier: 0, name: 'default' };
// Elite Tier
if ((fPerc < 0.2 && fTotal >= 10000) || (fPerc < 0.1 && fTotal >= 5000)) {
v = { tier: 5, name: 'elite' };
}
// "Master" Tier (accurate gold flagger, but not many flags compared to elites)
else if ((fPerc < 0.5 && fTotal >= 2000) || (fPerc < 0.2 && fTotal >= 1000)) {
v = { tier: 4, name: 'elite' };
}
// "Diamond" Tier (accurate silver flagger, but not many flags compared to elites)
else if ((fPerc < 0.5 && fTotal >= 1000) || (fPerc < 0.3 && fTotal >= 500)) {
v = { tier: 1, name: 'gold' };
}
// "Rising Star" Tier (accurate bronze flagger, but not many flags compared to elites)
else if ((fPerc < 0.5 && fTotal >= 500) || (fPerc < 0.4 && fTotal >= 200)) {
v = { tier: 1, name: 'silver' };
}
// Gold Tier
else if ((fPerc < 1 && fTotal >= 2000) || (fPerc < 0.5 && fTotal >= 1000)) {
v = { tier: 3, name: 'gold' };
}
// Silver Tier
else if ((fPerc < 3 && fTotal >= 1000) || (fPerc < 1.5 && fTotal >= 500)) {
v = { tier: 2, name: 'silver' };
}
// Bronze Tier
else if ((fPerc < 5 && fTotal >= 500) || (fPerc < 2.5 && fTotal >= 200)) {
v = { tier: 1, name: 'bronze' };
}
// Wtf Tier
else if (fPerc >= 30 && fTotal >= 1) {
v = { tier: -3, name: 'wtf' };
}
// Horrible Tier
else if (fPerc >= 15 && fTotal >= 1) {
v = { tier: -2, name: 'horrible' };
}
// Hmmm Tier
else if (fPerc >= 5 && fTotal >= 1) {
v = { tier: -1, name: 'hmmm' };
}
return v;
}
function getUserFlagStats(uid) {
const keyroot = 'ModFlaggerStats';
const fullkey = `${keyroot}:${uid}`;
let v = JSON.parse(store.getItem(fullkey));
return new Promise(function (resolve, reject) {
if (v != null) { resolve(v); return; }
$.ajax(`${location.origin}/users/flag-summary/${uid}`)
.done(function (data) {
const rep = repStrToNumeric($('.user-details .reputation-score', data).text());
const infotable = $('#sidebar .s-sidebarwidget--item.d-block', data);
let fTotal = 0, fTotalElem = infotable.find('a .ta-right').first();
if (fTotalElem.length != 0) fTotal = Number(fTotalElem.text().replace(/[^\d]+/g, ''));
let fDeclined = 0, fDeclinedElem = infotable.find('a[href="?group=1&status=3"] .ta-right').first();
if (fDeclinedElem.length != 0) fDeclined = Number(fDeclinedElem.text().replace(/[^\d]+/g, ''));
const fPerc = Number((fDeclined / (fTotal || 1) * 100).toFixed(2));
// store regular good flaggers
if ((fPerc < 1 && fTotal >= 1000) || fTotal >= 10000) {
store.setItem(fullkey, JSON.stringify([rep, fTotal, fDeclined, fPerc]));
}
resolve([rep, fTotal, fDeclined, fPerc]);
})
.fail(reject);
});
}
function loadFlaggingFn() {
if ($(this).hasClass('js-userflagstats-loaded') || $(this).hasClass('js-userflagstats-loading')) return;
const uid = this.dataset.uid;
const sameUserLinks = $(`.js-post-flag-group a[href^="/users/${uid}/"]`).addClass('js-userflagstats-loading');
const currLink = $(this).addClass('js-userflagstats-loading');
getUserFlagStats(uid).then(function (v) {
const tier = calculateFlagTier(v[1], v[3]);
const badge = ``;
// Apply to all instances of same user on page
sameUserLinks.not('js-userflagstats-loaded').addClass('js-userflagstats-loaded').after(badge);
});
}
// Append styles
addStylesheet(`
#mainbar-full .fs-body3:first-child button {
margin-top: -2px;
margin-left: 10px;
}
.flag-badge {
margin-left: 2px;
color: var(--black) !important;
}
.flag-badge + .flag-badge {
display: none;
}
.flag-badge.elite {
color: var(--green-500) !important;
}
.flag-badge.gold {
color: var(--gold) !important;
}
.flag-badge.silver {
color: var(--silver) !important;
}
.flag-badge.bronze {
color: var(--bronze) !important;
}
.flag-badge.wtf {
color: var(--red-500) !important;
}
.flag-badge.horrible {
color: var(--red-400) !important;
}
.flag-badge.hmmm {
color: var(--red-300) !important;
}
.flag-badge.default path {
fill: none;
stroke: var(--black);
stroke-width: 0.8px;
stroke-dasharray: 1,1;
stroke-linejoin: round;
}
.flag-badge.large {
display: inherit;
scale: 150%;
}
.flag-badge.default.large path {
stroke-dasharray: 2,1;
}
.flag-badge .svg-icon {
vertical-align: text-bottom;
}
`); // end stylesheet
// On script run
(function init() {
let currUid = selfId;
// If deleted user, do nothing
if (document.title.indexOf('User deleted') >= 0) return;
// If on user profile page
if (/\/users\/\d+\/.*/.test(location.pathname) && (location.search === '' || location.search === '?tab=profile')) {
// If on own user profile page
if (location.pathname.indexOf('/users/' + currUid) === 0) {
currUid = selfId
}
// Else must be a mod
else if (isModerator()) {
currUid = Number(location.pathname.match(/\d+/)[0]);
}
else return;
getUserFlagStats(currUid).then(function (v) {
const tier = calculateFlagTier(v[1], v[3]);
const badge = ``;
$("#content .fs-headline2").after(badge);
});
}
// Non-mods, exit
if (!isModerator()) return;
// Load user stats on hover
const userlinks = $('.js-post-flag-group a[href^="/users/"]').on('loadflaggingstats', loadFlaggingFn);
// Ignore mods
$('.js-post-flag-group .mod-flair').prev('a').addClass('js-userflagstats-loaded').off('loadflaggingstats');
// Preprocess userlinks to get the uids
userlinks.each(function () {
this.dataset.uid = this.href.match(/-?\d+/)[0];
});
// Load user stats on post hover
$('.js-admin-dashboard').on('mouseover', '.js-flagged-post', function () {
$('.js-post-flag-group a', this).trigger('loadflaggingstats');
});
// Load all flagger stats button
if (isModPage()) {
const loadBtn = $('')
.appendTo('#mainbar-full .fs-body3:first')
.on('click', function () {
$(this).remove();
// unique loads
let uids = [];
const uniqusers = userlinks.filter(function () {
if (!uids.includes(this.dataset.uid)) {
uids.push(this.dataset.uid);
return true;
}
return false;
}).filter(function () {
// ignore those already loaded
return !$(this).hasClass('js-userflagstats-loading') && !$(this).hasClass('js-userflagstats-loaded');
});
// Do nothing if none needs loading
if (uniqusers.length === 0) return;
// Display progress
$('body').showAjaxProgress(uniqusers.length, { position: 'fixed' });
// Load each flagger info
uniqusers.each(loadFlaggingFn);
});
if (isSuperuser) {
loadBtn.trigger('click');
}
}
})();
// Debug functions to clear localStorage
const purgeUserFlagStats = () => {
_window.lsRemoveItemsWithPrefix('ModFlaggerStats');
};
_window.purgeUserFlagStats = purgeUserFlagStats;
// Clear flagger stats cache on even days
if (new Date().getDay() % 2 === 0) purgeUserFlagStats();