diff --git a/global-review-summary/README.md b/global-review-summary/README.md
new file mode 100644
index 0000000..4e1ea23
--- /dev/null
+++ b/global-review-summary/README.md
@@ -0,0 +1,23 @@
+# Global Review Summary
+
+This script adds a 'reviews' tab to your network profile, which will show your review
+totals across the network.
+
+![](example.png)
+
+## Installation
+
+- Make sure you have Tampermonkey (for Chrome)
+or Greasemonkey / Violentmonkey (for Firefox) installed.
+
+- Install the userscript with
+[this direct link](https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/global-review-summary/global-review-summary.user.js).
+
+## Support
+
+If you have any questions, please post a comment on [this Stack Apps question](https://stackapps.com/q//34061).
+
+## Attribution
+
+The code of this userscript leans heavily on the
+[Stack Exchange Global Flag Summary](https://stackapps.com/q/7173/34061) script by Floern.
\ No newline at end of file
diff --git a/global-review-summary/example.png b/global-review-summary/example.png
new file mode 100644
index 0000000..83769fc
Binary files /dev/null and b/global-review-summary/example.png differ
diff --git a/global-review-summary/global-review-summary.user.js b/global-review-summary/global-review-summary.user.js
new file mode 100644
index 0000000..87ee6d7
--- /dev/null
+++ b/global-review-summary/global-review-summary.user.js
@@ -0,0 +1,427 @@
+// ==UserScript==
+// @name Stack Exchange Global Review Summary
+// @version 0.1
+// @description Stack Exchange network wide review summary available in your network profile
+// @author Glorfindel
+// @attribution Floern (https://github.com/Floern)
+// @include *://stackexchange.com/users/*/*
+// @match *://*.stackexchange.com/review*
+// @match *://*.stackoverflow.com/review*
+// @match *://*.superuser.com/review*
+// @match *://*.serverfault.com/review*
+// @match *://*.askubuntu.com/review*
+// @match *://*.stackapps.com/review*
+// @match *://*.mathoverflow.net/review*
+// @connect stackexchange.com
+// @connect stackoverflow.com
+// @connect superuser.com
+// @connect serverfault.com
+// @connect askubuntu.com
+// @connect stackapps.com
+// @connect mathoverflow.net
+// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
+// @grant GM.xmlHttpRequest
+// @grant GM_xmlhttpRequest
+// @grant GM.addStyle
+// @grant GM_addStyle
+// @run-at document-end
+// ==/UserScript==
+
+let reviewSummaryTable, reviewSummaryTableBody, errorView;
+
+let sortedColIndex = 10;
+let sortedColAsc = false;
+
+let reviewURIs = ["first-posts", "late-answers", "low-quality-posts", "suggested-edits",
+ "close", "reopen", "triage", "helper"];
+
+let reviewGlobalSummaryTotals = new Array(reviewURIs.length).fill(0);
+
+let totalsPerSite = {};
+let summariesPerSite = {};
+
+// init
+(function () {
+ if (window.location.href.match(/\/review\W?/i)) {
+ showGlobalReviewSummaryLink();
+ return;
+ }
+
+ if (!window.location.href.match(/:\/\/stackexchange\.com\/users\/\d+/i)) {
+ return;
+ }
+
+ let navigation = document.querySelector('#content .contentWrapper .subheader');
+ if (!navigation) {
+ return;
+ }
+
+ let tabbar = navigation.querySelector('.tabs');
+
+ // verify that we are in the profile of the logged in user
+ let tabs = tabbar.getElementsByTagName('a');
+ let loggedIn = false;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].textContent.trim().toLowerCase() == 'inbox') {
+ loggedIn = true;
+ break;
+ }
+ }
+ if (!loggedIn) {
+ return;
+ }
+
+ // add navigation tab for reviews
+ let reviewTab = document.createElement('a');
+ reviewTab.setAttribute('href', '?tab=reviews');
+ reviewTab.textContent = 'reviews';
+ tabs[4].parentNode.insertBefore(reviewTab, tabs[4]);
+
+ if (!window.location.href.match(/:\/\/stackexchange\.com\/users\/\d+\/.+?\?tab=reviews/i)) {
+ return;
+ }
+
+ // unselect default tab
+ let selectedTab = navigation.querySelector('.youarehere');
+ selectedTab.className = '';
+
+ // set selected tab to reviews
+ reviewTab.className = 'youarehere';
+
+ // remove default content
+ while (navigation.nextSibling) {
+ navigation.parentNode.removeChild(navigation.nextSibling);
+ }
+
+ document.querySelector('title').textContent = 'Review Summary - Stack Exchange';
+
+ let container = document.createElement('div');
+ navigation.parentNode.appendChild(container);
+
+ // setup summary table
+ reviewSummaryTable = document.createElement('table');
+ reviewSummaryTable.id = 'review-summary-table';
+ reviewSummaryTable.style.width = '100%';
+ reviewSummaryTable.style.textAlign = 'right';
+ reviewSummaryTable.style.borderCollapse = 'separate';
+ reviewSummaryTable.style.borderSpacing = '0 5px';
+ reviewSummaryTable.innerHTML = `
+
+
+ Site |
+ FP |
+ LA |
+ LQP |
+ SE |
+ CV |
+ RV |
+ Tr |
+ H&I |
+ total |
+
+
+
+
+
+
+ `;
+ container.appendChild(reviewSummaryTable);
+
+ // make columns sortable
+ let tableLabelNodes = reviewSummaryTable.querySelectorAll('#review-summary-heading-labels th');
+ for (let i = 0; i < tableLabelNodes.length; i++) {
+ let col = i + 1;
+ tableLabelNodes[i].onclick = function() {
+ sortedColAsc = col == sortedColIndex ? !sortedColAsc : false;
+ sortTable(col, sortedColAsc);
+ }
+ }
+
+ reviewSummaryTableBody = reviewSummaryTable.getElementsByTagName('tbody')[0];
+
+ // some table CSS
+ GM.addStyle("#review-summary-table tbody tr:hover { background: rgba(127, 127, 127, .10); }");
+ GM.addStyle("#review-summary-global-stats th { border-bottom: 1px #ddd solid; }");
+ GM.addStyle("#review-summary-table tbody tr { counter-increment: siteNumber; }");
+ GM.addStyle("#review-summary-table tbody tr td:first-child::before { content: counter(siteNumber); width: 14px; " +
+ "margin-right: 10px; color: #bbb; font-size: 10px; display: inline-block; text-align: right; margin-left: -24px; }");
+
+ // init global review summary
+ updateGlobalReviewStats();
+
+ // prepare error view
+ errorView = document.createElement('div');
+ container.appendChild(errorView);
+
+ // create loading view
+ let loadingView = document.createElement("div");
+ loadingView.id = 'review-summary-loading';
+ loadingView.style.textAlign = 'center';
+ loadingView.innerHTML = '
' +
+ '';
+ container.appendChild(loadingView);
+
+ // load data
+ loadAccountList();
+})();
+
+/**
+ * Add a link to the review summary page.
+ */
+function showGlobalReviewSummaryLink() {
+ // add link to header
+ let link = document.createElement('a');
+ link.setAttribute('href', '//stackexchange.com/users/current?tab=reviews');
+ link.textContent = 'global summary';
+ var tabs = document.getElementById("tabs");
+ if (tabs != null) {
+ tabs.appendChild(link);
+ } else {
+ var header = document.querySelector('#content .subheader');
+ header.setAttribute('style', 'display: table');
+ link.setAttribute('style', 'padding-left: 20px; display: table-cell; vertical-align: middle;');
+ header.appendChild(link);
+ }
+}
+
+/**
+ * Update global review summary in header.
+ */
+function updateGlobalReviewStats() {
+ var html = ` | `;
+ var grandTotal = 0;
+ for (var i = 0; i < reviewURIs.length; i++) {
+ grandTotal += reviewGlobalSummaryTotals[i];
+ html += `` + reviewGlobalSummaryTotals[i].toLocaleString() + ` | `
+ }
+ html += `` + grandTotal.toLocaleString() + ` | `;
+ document.getElementById('review-summary-global-stats').innerHTML = html;
+}
+
+/**
+ * Load the network account list.
+ */
+function loadAccountList() {
+ let accountListUrl = '//stackexchange.com/users/current?tab=accounts';
+ GM.xmlHttpRequest({
+ method: 'GET',
+ url: accountListUrl,
+ onload: function(response) {
+ parseNetworkAccounts(response.response);
+ },
+ onerror: function(response) {
+ console.error('loadAccountList: ' + JSON.stringify(response));
+ showLoadingError(accountListUrl, response.status);
+ }
+ });
+}
+
+/**
+ * Parse the network account list.
+ */
+function parseNetworkAccounts(html) {
+ let pageNode = document.createElement('div');
+ pageNode.innerHTML = html;
+
+ let accounts = [];
+
+ // iterate all accounts
+ let accountNodes = pageNode.querySelectorAll('.contentWrapper .account-container');
+ for (let i = 0; i < accountNodes.length; ++i) {
+ let accountNode = accountNodes[i];
+
+ let siteLinkNode = accountNode.querySelector('.account-site a');
+ if (!siteLinkNode) {
+ continue;
+ }
+ if (siteLinkNode.href.indexOf('area51.stackexchange.com/') != -1) {
+ // use area51.meta.SE instead
+ siteLinkNode.href = siteLinkNode.href.replace('//area51.st', '//area51.meta.st');
+ }
+
+ let siteName = siteLinkNode.textContent.trim();
+ let siteReviewURL = siteLinkNode.href.replace(/users\/.*$/i, 'review/');
+
+ // get reputation
+ let reputationNode = accountNode.querySelector('.account-stat span.account-number');
+ if (!reputationNode) {
+ continue;
+ }
+ let reputation = parseInt(reputationNode.innerHTML.replace(",", ""));
+ // 350 is the minimum (for beta sites)
+ if (reputation < 350) {
+ continue;
+ }
+
+ accounts.push({ siteName: siteName, reviewURL: siteReviewURL });
+
+ // add meta site
+ if (!/(meta\.stackexchange|stackapps)\.com\//.test(siteReviewURL)) {
+ let metaSiteReviewURL;
+ if (/\.stackexchange\.com\//.test(siteReviewURL)) // SE 2.0 sites
+ metaSiteReviewURL = siteReviewURL.replace('.stackexchange.com', '.meta.stackexchange.com');
+ else if (/\/\/[a-z]{2}\.stackoverflow\.com\//.test(siteReviewURL)) // localized SO sites
+ metaSiteReviewURL = siteReviewURL.replace('.stackoverflow.com', '.meta.stackoverflow.com');
+ else // SE 1.0 sites
+ metaSiteReviewURL = siteReviewURL.replace('//', '//meta.');
+ accounts.push({ siteName: siteName + " Meta", reviewURL: metaSiteReviewURL });
+ }
+ }
+
+ // load the sites
+ let i = -1;
+ let loaded = 0;
+ function loadNextSite() {
+ i++;
+ if (i >= accounts.length) {
+ // end of list
+ return;
+ }
+
+ let account = accounts[i];
+ let delay = 1000;
+ setTimeout(function() {
+ loadSiteReviewSummary(account.siteName, account.reviewURL, function() {
+ loaded++;
+ document.getElementById('review-summary-loading-progress').textContent = loaded + " / " + accounts.length;
+ if (loaded >= accounts.length) {
+ // end of list
+ document.getElementById('review-summary-loading').style.visibility = 'hidden';
+ }
+ loadNextSite();
+ }, 0);
+ }, delay);
+ };
+
+ // start 3 'threads' in parallel
+ loadNextSite();
+ loadNextSite();
+ loadNextSite();
+}
+
+/**
+ * Load the review summary of the specified site.
+ */
+function loadSiteReviewSummary(siteName, siteReviewURL, finishedCallback, index) {
+ console.log('loading ' + siteName + ', ' + reviewURIs[index]);
+ var url = siteReviewURL + reviewURIs[index] + '/stats';
+ GM.xmlHttpRequest({
+ method: 'GET',
+ url: url,
+ onload: function(response) {
+ if (++index == reviewURIs.length) {
+ finishedCallback();
+ } else {
+ loadSiteReviewSummary(siteName, siteReviewURL, finishedCallback, index)
+ }
+ if (response.status < 400) {
+ parseSiteReviewSummary(siteName, siteReviewURL, response.response, index - 1);
+ } else {
+ showLoadingError(siteReviewURL, response.status);
+ }
+ },
+ onerror: function(response) {
+ console.error('loadSiteReviewSummary: ' + url);
+ console.error('loadSiteReviewSummary: ' + JSON.stringify(response));
+ if (++index == reviewURIs.length) {
+ finishedCallback();
+ } else {
+ loadSiteReviewSummary(siteName, siteReviewURL, finishedCallback, index)
+ }
+ showLoadingError(siteReviewURL, response.status);
+ }
+ });
+}
+
+/**
+ * Parse the review site and extract the stats.
+ */
+function parseSiteReviewSummary(siteName, siteReviewURL, html, index) {
+ let pageNode = document.createElement('div');
+ pageNode.innerHTML = html;
+
+ // Determine # of reviews
+ let count = pageNode.querySelector("#badge-progress-count").innerText;
+ let reviews = parseInt(count.replace(",", ""), 10);
+ if (reviews == 0) {
+ // skip when no reviews
+ return;
+ }
+
+ // Collect totals
+ if (typeof(totalsPerSite[siteName]) === "undefined") {
+ totalsPerSite[siteName] = reviews;
+ } else {
+ totalsPerSite[siteName] += reviews;
+ }
+
+ // update global summary
+ reviewGlobalSummaryTotals[index] += reviews;
+ updateGlobalReviewStats();
+
+ let siteReviewSummaryTr = summariesPerSite[siteName];
+ if (typeof(siteReviewSummaryTr) === "undefined") {
+ // create table row for this site
+ siteReviewSummaryTr = document.createElement('tr');
+ let siteFaviconURL = pageNode.querySelector('link[rel*="icon"]').href;
+ siteReviewSummaryTr.innerHTML = `
+ |
+ ` + siteName + ` | `
+ for (var i = 0; i < reviewURIs.length; i++) {
+ siteReviewSummaryTr.innerHTML += ` | `
+ }
+ siteReviewSummaryTr.innerHTML += ` | `;
+ reviewSummaryTableBody.appendChild(siteReviewSummaryTr);
+ summariesPerSite[siteName] = siteReviewSummaryTr;
+ }
+ siteReviewSummaryTr.querySelector("." + reviewURIs[index]).innerText = reviews.toLocaleString();
+ siteReviewSummaryTr.querySelector(".total").innerText = totalsPerSite[siteName].toLocaleString();
+
+ // keep order
+ sortTable(sortedColIndex, sortedColAsc);
+}
+
+/**
+ * Find the previous non-text sibling node.
+ */
+function previousElementSibling(node) {
+ do {
+ node = node.previousSibling;
+ } while (node && node.nodeType !== 1);
+ return node;
+}
+
+/**
+ * Show an error.
+ */
+function showLoadingError(url, statuscode) {
+ let errorMsg = document.createElement("div");
+ errorMsg.innerHTML = 'Failed to load ' + url + ' with status ' + statuscode + '';
+ errorView.appendChild(errorMsg);
+}
+
+/**
+ * Sort the table by column index `col` and bool `asc`.
+ */
+function sortTable(col, asc) {
+ sortedColIndex = col;
+ let trs = Array.prototype.slice.call(reviewSummaryTableBody.rows, 0);
+ asc = -((+asc) || -1);
+ if (col == 1) {
+ trs = trs.sort(function (a, b) {
+ return asc * (a.cells[col].textContent.trim().localeCompare(b.cells[col].textContent.trim()));
+ });
+ }
+ else {
+ trs = trs.sort(function (a, b) {
+ let va = parseInt(a.cells[col].textContent.replace(/\D/g, '')) || 0;
+ let vb = parseInt(b.cells[col].textContent.replace(/\D/g, '')) || 0;
+ if (va != vb) // primary order
+ return asc * (vb - va);
+ else // secondary order
+ return a.cells[1].textContent.trim().localeCompare(b.cells[1].textContent.trim());
+ });
+ }
+ for (let i = 0; i < trs.length; ++i) reviewSummaryTableBody.appendChild(trs[i]);
+}