Add option to show the number of recent posts made by the OP

Add an option to show the number of recent posts made by the OP next to
their username in threads (profileindicator_alt).

If this option and the profileindicator options are set, then the
indicator dot and the recent posts number badge will be merged into a
single component.

Change-Id: If2fb1e8f0066d75ef136b6f93869b7fc2f0c7e57
diff --git a/docs/op_indicator.es.md b/docs/op_indicator.es.md
new file mode 100644
index 0000000..deeed5c
--- /dev/null
+++ b/docs/op_indicator.es.md
@@ -0,0 +1,26 @@
+# Indicador para el autor del hilo
+Esta es una función que muestra un indicador en los hilos al lado del nombre de
+usuario del autor, para ayudar a los EPs saber si el autor ha participado en
+otros hilos, lo que ayuda a encontrar hilos duplicados o obtener más contexto
+sobre el problema del usuario al poder visitar los otros hilos donde ha
+publicado o respondido.
+
+Hay dos opciones, que usan métodos e indicadores diferentes para ayudarte a
+determinar si el autor del hilo ha participado en otros hilos:
+
+1. La primera opción busca en el Foro actual los 5 hilos más recientes donde el
+autor del hilo ha participado. Después, dependiendo de la lista de hilos
+devuelta, se muestra un punto al lado de su nombre de usuario que muestra uno
+de los estados siguientes:
+  * Punto azul: si la búsqueda solo devolvió el hilo actual.
+  * Punto naranja: si la búsqueda devolvió más hilos, pero los otros hilos se
+  han marcado como leídos.
+  * Punto rojo: si la búsqueda devolvió más hilos, pero al menos uno de ellos no
+  está marcado como leído.
+2. La segunda opción hace una petición para cargar el perfil del usuario en vez
+de buscar en el foro los hilos en sí. Esto devuelve el número de publicaciones
+(incluyendo los nuevos hilos, respuestas normales y respuestas recomendadas)
+hechas por el usuario durante el último año, agregadas por mes. La extensión
+suma los valores para los `n` meses más recientes (`n` es un valor
+configurable) y muestra el número resultante al lado del nombre de usuario del
+autor del hilo.
diff --git a/docs/op_indicator.md b/docs/op_indicator.md
new file mode 100644
index 0000000..a0e8d0f
--- /dev/null
+++ b/docs/op_indicator.md
@@ -0,0 +1,25 @@
+[En español](op_indicator.es.md)
+
+# OP indicator
+This is a feature which shows an indicator in threads next to the OP's username,
+in order to help PEs notice whether the OP has participated in other threads,
+which helps PEs find duplicate threads or get more context about the user
+problem by visiting the other threads in which they posted/replied.
+
+There are two options, which use different methods and indicators to help you
+determine whether the OP participated in other threads:
+
+1. The first option searches in the current Forum for the 5 most recent posts
+in which the OP participated. Then, depending on the thread list returned, a dot
+is displayed next to their username with one of the following states:
+  * Blue dot: if the search only returned the current thread.
+  * Orange dot: if the search returned more threads, but the other threads are
+  marked as read.
+  * Red dot: if the search returned more threads, but at least one of the other
+  threads is not marked as read.
+2. The second option makes a request to load the user profile instead of
+searching in the forum the actual threads. This returns the number of posts
+(including new threads, normal replies and recommended replies) made by the user
+over the last year, aggregated by month. The extension sums the values for the
+`n` most recent months (`n` is a configurable value) and then shows this
+resulting number next to the OP's username.
diff --git a/src/_locales/ca/messages.json b/src/_locales/ca/messages.json
index b39a47a..7a24358 100644
--- a/src/_locales/ca/messages.json
+++ b/src/_locales/ca/messages.json
@@ -63,10 +63,22 @@
     "message": "Incrementa el contrast entre els fils llegits i no llegits a la Consola de la Comunitat.",
     "description": "Feature checkbox in the options page"
   },
+  "options_profileindicator_header": {
+    "message": "Punt indicador",
+    "description": "Heading for the profile indicator feature options"
+  },
+  "options_profileindicator_moreinfo": {
+    "message": "+info sobre les 2 darreres opcions",
+    "description": "Link to learn more about the profile indicator feature"
+  },
   "options_profileindicator": {
     "message": "Mostra <span class=\"help\" title=\"Si l'autor ha participat a altres fils, es mostrarà un punt vermell al costat del seu nom d'usuari. Si les publicacions més recents han estat llegides, es mostrarà un punt taronja. Pots posar el teu ratolí a sobre del punt per mostrar què vol dir el seu color.\">si l'autor del fil ha participat a altres fils</span>.",
     "description": "Feature checkbox in the options page"
   },
+  "options_profileindicatoralt": {
+    "message": "Mostra el nombre de preguntes i respostes escrites per l'autor del fil durant els últims <span id='profileindicatoralt_months--container'></span> mesos al costat del seu nom d'usuari.",
+    "description": "Feature checkbox in the options page"
+  },
   "options_stickysidebarheaders": {
     "message": "Fes que les capçaleres de la barra lateral de la Consola de la Comunitat es quedin enganxades a dalt (+info a <code>pekb/thread/60784834</code>).",
     "description": "Feature checkbox in the options page"
@@ -110,5 +122,9 @@
   "inject_profileindicator_other_posts_unread": {
     "message": "Aquest usuari ha participat en altres fils d'aquest fòrum, alguns dels quals no has llegit.",
     "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicatoralt_numposts": {
+    "message": "Nombre de preguntes i respostes en els $1 darrers mesos.",
+    "description": "Tooltip for the profile indicator dot."
   }
 }
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 3d558b4..b031666 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -67,10 +67,22 @@
     "message": "Increase contrast between read and unread threads in the Community Console.",
     "description": "Feature checkbox in the options page"
   },
+  "options_profileindicator_header": {
+    "message": "Indicator dot",
+    "description": "Heading for the profile indicator feature options"
+  },
+  "options_profileindicator_moreinfo": {
+    "message": "+info about the 2 previous options",
+    "description": "Link to learn more about the profile indicator feature"
+  },
   "options_profileindicator": {
     "message": "Show <span class=\"help\" title=\"If the OP has participated in other threads, a red dot will be shown next to their username. If the OP's most recent posts have been read, an orange dot will be shown instead. You can hover the dot with your mouse in order to show the meaning of its color.\">whether the OP has participated in other threads</span>.",
     "description": "Feature checkbox in the options page"
   },
+  "options_profileindicatoralt": {
+    "message": "Show the number of questions and replies written by the OP within the last <span id='profileindicatoralt_months--container'></span> months next to their username.",
+    "description": "Feature checkbox in the options page"
+  },
   "options_save": {
     "message": "Save",
     "description": "Button in the options page to save the settings"
@@ -110,5 +122,9 @@
   "inject_profileindicator_other_posts_unread": {
     "message": "The OP participated in other threads in this forum, some of which you haven't read.",
     "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicatoralt_numposts": {
+    "message": "Number of questions and replies in the last $1 months.",
+    "description": "Tooltip for the profile indicator dot."
   }
 }
diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json
index 6eedd25..d1103c1 100644
--- a/src/_locales/es/messages.json
+++ b/src/_locales/es/messages.json
@@ -63,10 +63,22 @@
     "message": "Incrementa el contraste entre los hilos leídos y no leídos en la Consola de la Comunidad.",
     "description": "Feature checkbox in the options page"
   },
+  "options_profileindicator_header": {
+    "message": "Punto indicador",
+    "description": "Heading for the profile indicator feature options"
+  },
+  "options_profileindicator_moreinfo": {
+    "message": "+info sobre las 2 opciones anteriores",
+    "description": "Link to learn more about the profile indicator feature"
+  },
   "options_profileindicator": {
     "message": "Muestra <span class=\"help\" title=\"Si el autor ha participado en otros hilos, se mostrará un punto rojo al lado de su nombre de usuario. Si las publicaciones más recientes se han leído, se mostrará un punto naranja. Puedes poner tu ratón encima del punto para mostrar qué quiere decir su color.\">si el autor del hilo ha participado en otros hilos</span>.",
     "description": "Feature checkbox in the options page"
   },
+  "options_profileindicatoralt": {
+    "message": "Muestra el número de preguntas y respuestas escritas por el autor del hilo durante los últimos <span id='profileindicatoralt_months--container'></span> meses al lado de su nombre de usuario.",
+    "description": "Feature checkbox in the options page"
+  },
   "options_stickysidebarheaders": {
     "message": "Hacer que los encabezados de la barra lateral de la Consola de la Comunidad se queden pegados arriba (+info en <code>pekb/thread/60784834</code>).",
     "description": "Feature checkbox in the options page"
@@ -110,5 +122,9 @@
   "inject_profileindicator_other_posts_unread": {
     "message": "Este usuario ha participado en otros hilos de este foro, algunos de los cuales no has leído.",
     "description": "Tooltip for the profile indicator dot."
+  },
+  "inject_profileindicatoralt_numposts": {
+    "message": "Número de preguntas y respuestas en los últimos $1 meses.",
+    "description": "Tooltip for the profile indicator dot."
   }
 }
diff --git a/src/common/common.js b/src/common/common.js
index 28c233a..8845b81 100644
--- a/src/common/common.js
+++ b/src/common/common.js
@@ -12,8 +12,14 @@
   'increasecontrast': false,
   'stickysidebarheaders': false,
   'profileindicator': false,
+  'profileindicatoralt': false,
+  'profileindicatoralt_months': 12,
 };
 
+const specialOptions = [
+  'profileindicatoralt_months',
+];
+
 const deprecatedOptions = [
   'escalatethreads',
   'movethreads',
@@ -26,11 +32,16 @@
 
 function cleanUpOptions(options) {
   console.log('[cleanUpOptions] Previous options', options);
-  var ok = true;
-  for (const [opt, value] of Object.entries(defaultOptions)) {
-    if (!(opt in options)) {
-      ok = false;
-      options[opt] = value;
+
+  if (typeof options !== 'object' || options === null) {
+    options = defaultOptions;
+  } else {
+    var ok = true;
+    for (const [opt, value] of Object.entries(defaultOptions)) {
+      if (!(opt in options)) {
+        ok = false;
+        options[opt] = value;
+      }
     }
   }
 
diff --git a/src/content_scripts/console_inject.js b/src/content_scripts/console_inject.js
index 14556b8..eb0975b 100644
--- a/src/content_scripts/console_inject.js
+++ b/src/content_scripts/console_inject.js
@@ -53,9 +53,11 @@
 
               var name = escapeUsername(nameElement.innerHTML);
               var query1 = encodeURIComponent(
-                  '(creator:"' + name + '" | replier:"' + name + '") forum:'+forumId);
+                  '(creator:"' + name + '" | replier:"' + name +
+                  '") forum:' + forumId);
               var query2 = encodeURIComponent(
-                  '(creator:"' + name + '" | replier:"' + name + '") forum:any');
+                  '(creator:"' + name + '" | replier:"' + name +
+                  '") forum:any');
               addProfileHistoryLink(node, 'forum', query1);
               addProfileHistoryLink(node, 'all', query2);
             }
@@ -109,23 +111,4 @@
     injectStyles(
         'material-drawer .main-header{background: #fff; position: sticky; top: 0; z-index: 1;}');
   }
-
-  if (options.profileindicator) {
-    injectScript(
-        chrome.runtime.getURL('injections/profileindicator_inject.js'));
-    injectStylesheet(
-        chrome.runtime.getURL('injections/profileindicator_inject.css'));
-
-    // In order to pass i18n strings to the injected script, which doesn't have
-    // access to the chrome.i18n API.
-    window.addEventListener('geti18nString', evt => {
-      var request = evt.detail;
-      var response = {
-        string: chrome.i18n.getMessage(request.msg),
-        requestId: request.id
-      };
-      window.dispatchEvent(
-          new CustomEvent('sendi18nString', {detail: response}));
-    });
-  }
 });
diff --git a/src/content_scripts/profileindicator_inject.js b/src/content_scripts/profileindicator_inject.js
new file mode 100644
index 0000000..c726783
--- /dev/null
+++ b/src/content_scripts/profileindicator_inject.js
@@ -0,0 +1,41 @@
+chrome.storage.sync.get(null, function(options) {
+  if (options.profileindicator || options.profileindicatoralt) {
+    // In order to pass i18n strings and settings values to the injected script,
+    // which doesn't have access to the chrome.* APIs, we use event listeners.
+    window.addEventListener('TWPT_sendRequest', evt => {
+      var request = evt.detail;
+      switch (request.data.action) {
+        case 'geti18nMessage':
+          var data = chrome.i18n.getMessage(
+              request.data.msg,
+              (Array.isArray(request.data.placeholders) ?
+                   request.data.placeholders :
+                   []));
+          break;
+
+        case 'getProfileIndicatorOptions':
+          var data = {
+            'indicatorDot': options.profileindicator,
+            'numPosts': options.profileindicatoralt
+          };
+          break;
+
+        case 'getNumPostMonths':
+          var data = options.profileindicatoralt_months;
+          break;
+
+        default:
+          var data = 'unknownAction';
+          break;
+      }
+      var response = {data, requestId: request.id, prefix: 'TWPT'};
+
+      window.postMessage(response, '*');
+    });
+
+    injectScript(
+        chrome.runtime.getURL('injections/profileindicator_inject.js'));
+    injectStylesheet(
+        chrome.runtime.getURL('injections/profileindicator_inject.css'));
+  }
+});
diff --git a/src/content_scripts/thread_inject.js b/src/content_scripts/thread_inject.js
index 706277f..90eb421 100644
--- a/src/content_scripts/thread_inject.js
+++ b/src/content_scripts/thread_inject.js
@@ -40,25 +40,6 @@
             new IntersectionObserver(intersectionCallback, intersectionOptions);
         intersectionObserver.observe(allbutton);
       }
-
-      if (items.profileindicator) {
-        injectScript(
-            chrome.runtime.getURL('injections/profileindicator_inject.js'));
-        injectStylesheet(
-            chrome.runtime.getURL('injections/profileindicator_inject.css'));
-
-        // In order to pass i18n strings to the injected script, which doesn't
-        // have access to the chrome.i18n API.
-        window.addEventListener('geti18nString', evt => {
-          var request = evt.detail;
-          var response = {
-            string: chrome.i18n.getMessage(request.msg),
-            requestId: request.id
-          };
-          window.dispatchEvent(
-              new CustomEvent('sendi18nString', {detail: response}));
-        });
-      }
     }
   });
 }
diff --git a/src/injections/profileindicator_inject.css b/src/injections/profileindicator_inject.css
index c7bd00f..d004d93 100644
--- a/src/injections/profileindicator_inject.css
+++ b/src/injections/profileindicator_inject.css
@@ -1,4 +1,14 @@
-ec-user-link.name > *, .thread-message-header__name > span:nth-child(3) > * {
+@keyframes loading-indicator {
+  from {
+    opacity: 0.25;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+ec-user-link.name > *, .thread-message-header__name > *, .thread-message-header__name > span:nth-child(3) > * {
   vertical-align: middle;
 }
 
@@ -11,21 +21,11 @@
   text-decoration: none!important;
 }
 
-@keyframes loading-profile-indicator {
-  from {
-    opacity: 0.25;
-  }
-
-  to {
-    opacity: 1;
-  }
-}
-
 .profile-indicator.profile-indicator--loading a {
   color: #6f6f6f;
 
   animation-duration: 0.75s;
-  animation-name: loading-profile-indicator;
+  animation-name: loading-indicator;
   animation-iteration-count: infinite;
   animation-direction: alternate;
 }
@@ -44,5 +44,54 @@
 
 .profile-indicator a {
   opacity: 1;
-  transition: opacity 1s, color 1s;
+  transition: opacity .5s, color .5s;
+}
+
+.num-posts-indicator {
+  display: inline-block;
+  padding: 3px 7px;
+  margin-left: 4px;
+  border-radius: 16px;
+  height: 14px;
+  min-width: 12px;
+
+  font-size: 12px;
+  line-height: 14px;
+  text-align: center;
+
+  background-color: #E0E0E0;
+  color: black;
+  transition: background-color .5s;
+}
+
+.num-posts-indicator.num-posts-indicator--loading {
+  background-color: #6f6f6f;
+
+  animation-duration: 0.75s;
+  animation-name: loading-indicator;
+  animation-iteration-count: infinite;
+  animation-direction: alternate;
+}
+
+.num-posts-indicator.num-posts-indicator--first-post {
+  background-color: #BBDEFB;
+}
+
+.num-posts-indicator.num-posts-indicator--other-posts-read {
+  background-color: #FFE0B2;
+}
+
+.num-posts-indicator.num-posts-indicator--other-posts-unread {
+  color: white;
+  background-color: #F44336;
+}
+
+.num-posts-indicator .num-posts-indicator--num {
+  display: inline-block;
+  opacity: 1;
+  transition: opacity .5s;
+}
+
+.num-posts-indicator .num-posts-indicator--num.num-posts-indicator--num--loading {
+  opacity: 0;
 }
diff --git a/src/injections/profileindicator_inject.js b/src/injections/profileindicator_inject.js
index ce9187b..a2c6528 100644
--- a/src/injections/profileindicator_inject.js
+++ b/src/injections/profileindicator_inject.js
@@ -7,9 +7,9 @@
 const OP_OTHER_POSTS_UNREAD = 2;
 
 const OPClasses = {
-  0: 'profile-indicator--first-post',
-  1: 'profile-indicator--other-posts-read',
-  2: 'profile-indicator--other-posts-unread',
+  0: 'first-post',
+  1: 'other-posts-read',
+  2: 'other-posts-unread',
 };
 
 const OPi18n = {
@@ -18,10 +18,14 @@
   2: 'other_posts_unread',
 };
 
+const indicatorTypes = ['numPosts', 'indicatorDot'];
+
 // Filter used as a workaround to speed up the ViewForum request.
 const FILTER_ALL_LANGUAGES =
     'lang:(ar | bg | ca | "zh-hk" | "zh-cn" | "zh-tw" | hr | cs | da | nl | en | "en-au" | "en-gb" | et | fil | fi | fr | de | el | iw | hi | hu | id | it | ja | ko | lv | lt | ms | no | pl | "pt-br" | "pt-pt" | ro | ru | sr | sk | sl | es | "es-419" | sv | th | tr | uk | vi)';
 
+const numPostsForumArraysToSum = [3, 4];
+
 function isElementInside(element, outerTag) {
   while (element !== null && ('tagName' in element)) {
     if (element.tagName == outerTag) return true;
@@ -60,40 +64,62 @@
       .then(res => res.json());
 }
 
+function getProfile(userId, forumId) {
+  return fetch('https://support.google.com/s/community/api/ViewUser', {
+           'credentials': 'include',
+           'headers': {'content-type': 'text/plain; charset=utf-8'},
+           'body': JSON.stringify({
+             '1': userId,
+             '2': 0,
+             '3': forumId,
+           }),
+           'method': 'POST',
+           'mode': 'cors',
+         })
+      .then(res => res.json());
+}
+
 // Source:
 // https://stackoverflow.com/questions/33063774/communication-from-an-injected-script-to-the-content-script-with-a-response
-var i18nRequest = (function() {
+var contentScriptRequest = (function() {
   var requestId = 0;
 
-  function getMessage(msg) {
+  function sendRequest(data) {
     var id = requestId++;
 
     return new Promise(function(resolve, reject) {
       var listener = function(evt) {
-        if (evt.detail.requestId == id) {
+        if (evt.source === window && evt.data && evt.data.prefix === 'TWPT' &&
+            evt.data.requestId == id) {
           // Deregister self
-          window.removeEventListener('sendChromeData', listener);
-          resolve(evt.detail.string);
+          window.removeEventListener('message', listener);
+          resolve(evt.data.data);
         }
       };
 
-      window.addEventListener('sendi18nString', listener);
+      window.addEventListener('message', listener);
 
-      var payload = {msg: msg, id: id};
+      var payload = {data, id};
 
-      window.dispatchEvent(new CustomEvent('geti18nString', {detail: payload}));
+      window.dispatchEvent(
+          new CustomEvent('TWPT_sendRequest', {detail: payload}));
     });
   }
 
-  return {getMessage: getMessage};
+  return {sendRequest: sendRequest};
 })();
 
-// Create profile indicator dot with a loading state
-function createIndicatorDot(sourceNode, searchURL) {
+// Create profile indicator dot with a loading state, or return the numPosts
+// badge if it is already created.
+function createIndicatorDot(sourceNode, searchURL, options) {
+  if (options.numPosts) return document.querySelector('.num-posts-indicator');
   var dotContainer = document.createElement('span');
-  dotContainer.classList.add('profile-indicator');
-  dotContainer.classList.add('profile-indicator--loading');
-  i18nRequest.getMessage('inject_profileindicator_loading')
+  dotContainer.classList.add('profile-indicator', 'profile-indicator--loading');
+  contentScriptRequest
+      .sendRequest({
+        'action': 'geti18nMessage',
+        'msg': 'inject_profileindicator_loading'
+      })
       .then(string => dotContainer.setAttribute('title', string));
 
   var dotLink = document.createElement('a');
@@ -106,8 +132,38 @@
   return dotContainer;
 }
 
-// Handle the profile indicator
-function handleIndicatorDot(sourceNode, isCC) {
+// Create badge indicating the number of posts with a loading state
+function createNumPostsBadge(sourceNode, searchURL) {
+  var link = document.createElement('a');
+  link.href = searchURL;
+
+  var numPostsContainer = document.createElement('div');
+  numPostsContainer.classList.add(
+      'num-posts-indicator', 'num-posts-indicator--loading');
+  contentScriptRequest
+      .sendRequest({
+        'action': 'geti18nMessage',
+        'msg': 'inject_profileindicator_loading'
+      })
+      .then(string => numPostsContainer.setAttribute('title', string));
+
+  var numPostsSpan = document.createElement('span');
+  numPostsSpan.classList.add('num-posts-indicator--num');
+
+  numPostsContainer.appendChild(numPostsSpan);
+  link.appendChild(numPostsContainer);
+  sourceNode.parentNode.appendChild(link);
+  return numPostsContainer;
+}
+
+// Get options and then handle all the indicators
+function getOptionsAndHandleIndicators(sourceNode, isCC) {
+  contentScriptRequest.sendRequest({'action': 'getProfileIndicatorOptions'})
+      .then(options => handleIndicators(sourceNode, isCC, options));
+}
+
+// Handle the profile indicator dot
+function handleIndicators(sourceNode, isCC, options) {
   var escapedUsername = escapeUsername(
       (isCC ? sourceNode.innerHTML :
               sourceNode.querySelector('span').innerHTML));
@@ -118,7 +174,7 @@
     var CCLink = document.getElementById('onebar-community-console');
     if (CCLink === null) {
       console.error(
-          '[dotindicator] The user is not a PE so the dot indicator cannot be shown in TW.');
+          '[opindicator] The user is not a PE so the dot indicator cannot be shown in TW.');
       return;
     }
     var threadLink = CCLink.href;
@@ -126,11 +182,12 @@
 
   var forumUrlSplit = threadLink.split('/forum/');
   if (forumUrlSplit.length < 2) {
-    console.error('[dotindicator] Can\'t get forum id.');
+    console.error('[opindicator] Can\'t get forum id.');
     return;
   }
 
   var forumId = forumUrlSplit[1].split('/')[0];
+
   var query = '(replier:"' + escapedUsername + '" | creator:"' +
       escapedUsername + '") ' + FILTER_ALL_LANGUAGES;
   var encodedQuery =
@@ -139,46 +196,117 @@
       (isCC ? 'https://support.google.com/s/community/search/' +
                encodeURIComponent('query=' + encodedQuery) :
               document.location.pathname.split('/thread')[0] +
-               '/threads?thread_filter=' + encodedQuery)
+               '/threads?thread_filter=' + encodedQuery);
 
-  var dotContainer = createIndicatorDot(sourceNode, searchURL);
+  if (options.numPosts) {
+    var profileURL = new URL(sourceNode.href);
+    var userId =
+        profileURL.pathname.split(isCC ? 'user/' : 'profile/')[1].split('/')[0];
 
-  // Query threads in order to see what state the indicator should be in
-  getPosts(query, forumId)
-      .then(res => {
-        if (!('1' in res) || !('2' in res['1'])) {
-          throw new Error('Unexpected response.');
-          return;
-        }
+    var numPostsContainer = createNumPostsBadge(sourceNode, searchURL);
 
-        // Current thread ID
-        var threadUrlSplit = threadLink.split('/thread/');
-        if (threadUrlSplit.length < 2) throw new Error('Can\'t get thread id.');
+    getProfile(userId, forumId)
+        .then(res => {
+          if (!('1' in res) || !('2' in res[1])) {
+            throw new Error('Unexpected profile response.');
+            return;
+          }
 
-        var currId = threadUrlSplit[1].split('/')[0];
+          contentScriptRequest.sendRequest({'action': 'getNumPostMonths'})
+              .then(months => {
+                if (!options.indicatorDot)
+                  contentScriptRequest
+                      .sendRequest({
+                        'action': 'geti18nMessage',
+                        'msg': 'inject_profileindicatoralt_numposts',
+                        'placeholders': [months]
+                      })
+                      .then(
+                          string =>
+                              numPostsContainer.setAttribute('title', string));
 
-        var OPStatus = OP_FIRST_POST;
+                var numPosts = 0;
 
-        for (const thread of res['1']['2']) {
-          var id = thread['2']['1']['1'] || undefined;
-          if (id === undefined || id == currId) continue;
+                for (const index of numPostsForumArraysToSum) {
+                  if (!(index in res[1][2])) {
+                    throw new Error('Unexpected profile response.');
+                    return;
+                  }
 
-          var isRead = thread['6'] || false;
-          if (isRead)
-            OPStatus = Math.max(OP_OTHER_POSTS_READ, OPStatus);
-          else
-            OPStatus = Math.max(OP_OTHER_POSTS_UNREAD, OPStatus);
-        }
+                  var i = 0;
+                  for (const month of res[1][2][index].reverse()) {
+                    if (i == months) break;
+                    numPosts += month[3] || 0;
+                    ++i;
+                  }
+                }
 
-        dotContainer.classList.remove('profile-indicator--loading');
-        dotContainer.classList.add(OPClasses[OPStatus]);
-        i18nRequest.getMessage('inject_profileindicator_' + OPi18n[OPStatus])
-            .then(string => dotContainer.setAttribute('title', string));
-      })
-      .catch(
-          err => console.error(
-              '[dotindicator] Unexpected error. Couldn\'t load recent posts.',
-              err));
+                numPostsContainer.classList.remove(
+                    'num-posts-indicator--loading');
+                numPostsContainer.querySelector('span').classList.remove(
+                    'num-posts-indicator--num--loading');
+                numPostsContainer.querySelector('span').textContent = numPosts;
+              })
+              .catch(
+                  err => console.error('[opindicator] Unexpected error.', err));
+        })
+        .catch(
+            err => console.error(
+                '[opindicator] Unexpected error. Couldn\'t load profile.',
+                err));
+    ;
+  }
+
+  if (options.indicatorDot) {
+    var dotContainer = createIndicatorDot(sourceNode, searchURL, options);
+
+    // Query threads in order to see what state the indicator should be in
+    getPosts(query, forumId)
+        .then(res => {
+          if (!('1' in res) || !('2' in res['1'])) {
+            throw new Error('Unexpected thread list response.');
+            return;
+          }
+
+          // Current thread ID
+          var threadUrlSplit = threadLink.split('/thread/');
+          if (threadUrlSplit.length < 2)
+            throw new Error('Can\'t get thread id.');
+
+          var currId = threadUrlSplit[1].split('/')[0];
+
+          var OPStatus = OP_FIRST_POST;
+
+          for (const thread of res['1']['2']) {
+            var id = thread['2']['1']['1'] || undefined;
+            if (id === undefined || id == currId) continue;
+
+            var isRead = thread['6'] || false;
+            if (isRead)
+              OPStatus = Math.max(OP_OTHER_POSTS_READ, OPStatus);
+            else
+              OPStatus = Math.max(OP_OTHER_POSTS_UNREAD, OPStatus);
+          }
+
+          var dotContainerPrefix =
+              (options.numPosts ? 'num-posts-indicator' : 'profile-indicator');
+
+          if (!options.numPosts)
+            dotContainer.classList.remove(dotContainerPrefix + '--loading');
+          dotContainer.classList.add(
+              dotContainerPrefix + '--' + OPClasses[OPStatus]);
+          contentScriptRequest
+              .sendRequest({
+                'action': 'geti18nMessage',
+                'msg': 'inject_profileindicator_' + OPi18n[OPStatus]
+              })
+              .then(string => dotContainer.setAttribute('title', string));
+        })
+        .catch(
+            err => console.error(
+                '[opindicator] Unexpected error. Couldn\'t load recent posts.',
+                err));
+  }
 }
 
 if (CCRegex.test(location.href)) {
@@ -191,7 +319,7 @@
               CCProfileRegex.test(node.href) &&
               isElementInside(node, 'EC-QUESTION') && ('children' in node) &&
               node.children.length == 0) {
-            handleIndicatorDot(node, true);
+            getOptionsAndHandleIndicators(node, true);
           }
         });
       }
@@ -208,9 +336,10 @@
       document.querySelector('.scrollable-content'), observerOptions);
 } else {
   // We are in TW
-  var node = document.querySelector('.thread-question .user-info-display-name');
+  var node =
+      document.querySelector('.thread-question a.user-info-display-name');
   if (node !== null)
-    handleIndicatorDot(node, false);
+    getOptionsAndHandleIndicators(node, false);
   else
-    console.error('[dotindicator] Couldn\'t find username.');
+    console.error('[opindicator] Couldn\'t find username.');
 }
diff --git a/src/options.css b/src/options.css
index 69a410c..2ee205f 100644
--- a/src/options.css
+++ b/src/options.css
@@ -11,6 +11,10 @@
   text-align: center;
 }
 
+#profileindicatoralt_months {
+  width: 3em;
+}
+
 #save-indicator {
   text-align: center;
   margin-bottom: 16px;
diff --git a/src/options.html b/src/options.html
index cd325a0..6daa9ca 100644
--- a/src/options.html
+++ b/src/options.html
@@ -6,22 +6,29 @@
     <link rel="stylesheet" href="options.css">
   </head>
   <body>
-    <p>
-      <input type="checkbox" id="list"> <label for="list" data-i18n="list"></label><br>
-      <input type="checkbox" id="thread"> <label for="thread" data-i18n="thread"></label><br>
-      <input type="checkbox" id="threadall"> <label for="threadall" data-i18n="threadall"></label>
-    </p>
-    <h4 data-i18n="enhancements"></h4>
-    <p>
-      <input type="checkbox" id="fixedtoolbar"> <label for="fixedtoolbar" data-i18n="fixedtoolbar"></label><br>
-      <input type="checkbox" id="redirect"> <label for="redirect" data-i18n="redirect"></label> <span class="experimental-label" data-i18n="experimental_label"></span><br>
-      <input type="checkbox" id="history"> <label for="history" data-i18n="history"></label><br>
-      <input type="checkbox" id="loaddrafts"> <label for="loaddrafts" data-i18n="loaddrafts"></label> <span class="experimental-label" data-i18n="experimental_label"></span><br>
-      <input type="checkbox" id="increasecontrast"> <label for="increasecontrast" data-i18n="increasecontrast"></label><br>
-      <input type="checkbox" id="stickysidebarheaders"> <label for="stickysidebarheaders" data-i18n="stickysidebarheaders"></label><br>
-      <input type="checkbox" id="profileindicator"> <label for="profileindicator" data-i18n="profileindicator"></label> <span class="experimental-label" data-i18n="experimental_label"></span><br>
-    </p>
-    <p class="actions"><button id="save" data-i18n="save"></button></p>
+    <form>
+      <p>
+        <input type="checkbox" id="list"> <label for="list" data-i18n="list"></label><br>
+        <input type="checkbox" id="thread"> <label for="thread" data-i18n="thread"></label><br>
+        <input type="checkbox" id="threadall"> <label for="threadall" data-i18n="threadall"></label>
+      </p>
+      <h4 data-i18n="enhancements"></h4>
+      <p>
+        <input type="checkbox" id="fixedtoolbar"> <label for="fixedtoolbar" data-i18n="fixedtoolbar"></label><br>
+        <input type="checkbox" id="redirect"> <label for="redirect" data-i18n="redirect"></label> <span class="experimental-label" data-i18n="experimental_label"></span><br>
+        <input type="checkbox" id="history"> <label for="history" data-i18n="history"></label><br>
+        <input type="checkbox" id="loaddrafts"> <label for="loaddrafts" data-i18n="loaddrafts"></label> <span class="experimental-label" data-i18n="experimental_label"></span><br>
+        <input type="checkbox" id="increasecontrast"> <label for="increasecontrast" data-i18n="increasecontrast"></label><br>
+        <input type="checkbox" id="stickysidebarheaders"> <label for="stickysidebarheaders" data-i18n="stickysidebarheaders"></label><br>
+      </p>
+      <h4 data-i18n="profileindicator_header"></h4>
+      <p>
+        <input type="checkbox" id="profileindicator"> <label for="profileindicator" data-i18n="profileindicator"></label> <span class="experimental-label" data-i18n="experimental_label"></span><br>
+        <input type="checkbox" id="profileindicatoralt"> <label for="profileindicatoralt" data-i18n="profileindicatoralt"></label> <span class="experimental-label" data-i18n="experimental_label"></span><br>
+        <a href="https://gerrit.avm99963.com/plugins/gitiles/infinitegforums/+/refs/heads/master/docs/op_indicator.md" target="_blank" rel="noreferrer noopener" data-i18n="profileindicator_moreinfo"></a><br>
+      </p>
+      <p class="actions"><button id="save" data-i18n="save"></button></p>
+    </form>
     <div id="save-indicator"></div>
     <script src="common/common.js"></script>
     <script src="options.js"></script>
diff --git a/src/options.js b/src/options.js
index 64d1bad..47cecf6 100644
--- a/src/options.js
+++ b/src/options.js
@@ -1,11 +1,37 @@
 var savedSuccessfullyTimeout = null;
 
-function save() {
+const exclusiveOptions = [['thread', 'threadall']];
+
+function save(e) {
   var options = defaultOptions;
 
+  // Validation checks before saving
+  var months = document.getElementById('profileindicatoralt_months');
+  if (!months.checkValidity()) {
+    console.warn(months.validationMessage);
+    return;
+  }
+
+  e.preventDefault();
+
+  // Save
   Object.keys(options).forEach(function(opt) {
     if (deprecatedOptions.includes(opt)) return;
-    options[opt] = document.querySelector('#' + opt).checked || false;
+
+    if (specialOptions.includes(opt)) {
+      switch (opt) {
+        case 'profileindicatoralt_months':
+          options[opt] = document.getElementById(opt).value || 12;
+          break;
+
+        default:
+          console.warn('Unrecognized option: ' + opt);
+          break;
+      }
+      return;
+    }
+
+    options[opt] = document.getElementById(opt).checked || false;
   });
 
   chrome.storage.sync.set(options, function() {
@@ -30,14 +56,6 @@
               'options_' + el.getAttribute('data-i18n')));
 }
 
-function thread() {
-  if (document.querySelector('#thread').checked &&
-      document.querySelector('#threadall').checked) {
-    document.querySelector('#' + (this.id == 'thread' ? 'threadall' : 'thread'))
-        .checked = false;
-  }
-}
-
 window.addEventListener('load', function() {
   i18n();
 
@@ -45,14 +63,44 @@
     items = cleanUpOptions(items);
 
     Object.keys(defaultOptions).forEach(function(opt) {
-      if (items[opt] === true && !deprecatedOptions.includes(opt)) {
-        document.querySelector('#' + opt).checked = true;
+      if (deprecatedOptions.includes(opt)) return;
+
+      if (specialOptions.includes(opt)) {
+        switch (opt) {
+          case 'profileindicatoralt_months':
+            var input = document.createElement('input');
+            input.type = 'number';
+            input.id = 'profileindicatoralt_months';
+            input.max = '12';
+            input.min = '1';
+            input.value = items[opt];
+            input.required = true;
+            document.getElementById('profileindicatoralt_months--container')
+                .appendChild(input);
+            break;
+
+          default:
+            console.warn('Unrecognized option: ' + opt);
+            break;
+        }
+        return;
       }
+
+      if (items[opt] === true) document.getElementById(opt).checked = true;
     });
 
-    ['thread', 'threadall'].forEach(
-        el => document.querySelector('#' + el).addEventListener(
-            'change', thread));
+    exclusiveOptions.forEach(exclusive => {
+      exclusive.forEach(
+          el => document.getElementById(el).addEventListener('change', e => {
+            if (document.getElementById(exclusive[0]).checked &&
+                document.getElementById(exclusive[1]).checked) {
+              document
+                  .getElementById(
+                      exclusive[(e.currentTarget.id == exclusive[0] ? 1 : 0)])
+                  .checked = false;
+            }
+          }));
+    });
     document.querySelector('#save').addEventListener('click', save);
   });
 });
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 83cc8ba..60c3feb 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -30,6 +30,10 @@
       "run_at": "document_end"
     },
     {
+      "matches": ["https://support.google.com/s/community*", "https://support.google.com/*/thread/*"],
+      "js": ["common/content_scripts.js", "content_scripts/profileindicator_inject.js"]
+    },
+    {
       "matches": ["https://support.google.com/*/profile/*"],
       "js": ["content_scripts/profile_inject.js"]
     }