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/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);
   });
 });