Bring the profile indicator feature to TW

Adapted the existing code in order for it to be injected too in TW.

Change-Id: I01720c040f7bbab892f02a8f2935ebcaeb01b68f
diff --git a/src/_locales/ca/messages.json b/src/_locales/ca/messages.json
index ad6f006..66a6349 100644
--- a/src/_locales/ca/messages.json
+++ b/src/_locales/ca/messages.json
@@ -64,7 +64,7 @@
     "description": "Feature checkbox in the options page"
   },
   "options_profileindicator": {
-    "message": "Mostra a la Consola de la Comunitat <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>.",
+    "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_stickysidebarheaders": {
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index f5c5cde..329c373 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -68,7 +68,7 @@
     "description": "Feature checkbox in the options page"
   },
   "options_profileindicator": {
-    "message": "Show in the Community Console <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>.",
+    "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_save": {
diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json
index aacc251..891f0a5 100644
--- a/src/_locales/es/messages.json
+++ b/src/_locales/es/messages.json
@@ -64,7 +64,7 @@
     "description": "Feature checkbox in the options page"
   },
   "options_profileindicator": {
-    "message": "Muestra en la Consola de la Comunidad <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>.",
+    "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_stickysidebarheaders": {
diff --git a/src/content_scripts/console_inject.js b/src/content_scripts/console_inject.js
index 62768bd..8d481e9 100644
--- a/src/content_scripts/console_inject.js
+++ b/src/content_scripts/console_inject.js
@@ -117,9 +117,9 @@
 
   if (options.profileindicator) {
     injectScript(
-        chrome.runtime.getURL('injections/console_profileindicator_inject.js'));
-    injectStylesheet(chrome.runtime.getURL(
-        'injections/console_profileindicator_inject.css'));
+        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.
diff --git a/src/content_scripts/thread_inject.js b/src/content_scripts/thread_inject.js
index b94f405..2e2554a 100644
--- a/src/content_scripts/thread_inject.js
+++ b/src/content_scripts/thread_inject.js
@@ -1,5 +1,22 @@
 var intersectionObserver;
 
+function injectStylesheet(stylesheetName) {
+  var link = document.createElement('link');
+  link.setAttribute('rel', 'stylesheet');
+  link.setAttribute('href', stylesheetName);
+  document.head.appendChild(link);
+}
+
+function injectStyles(css) {
+  injectStylesheet('data:text/css;charset=UTF-8,' + encodeURIComponent(css));
+}
+
+function injectScript(scriptName) {
+  var script = document.createElement('script');
+  script.src = scriptName;
+  document.head.appendChild(script);
+}
+
 function intersectionCallback(entries, observer) {
   entries.forEach(entry => {
     if (entry.isIntersecting) {
@@ -38,5 +55,24 @@
           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/console_profileindicator_inject.js b/src/injections/console_profileindicator_inject.js
deleted file mode 100644
index 0a44137..0000000
--- a/src/injections/console_profileindicator_inject.js
+++ /dev/null
@@ -1,173 +0,0 @@
-var profileRegex =
-    /^(?:https:\/\/support\.google\.com)?\/s\/community\/forum\/[0-9]*\/user\/(?:[0-9]+)$/;
-
-const OP_FIRST_POST = 0;
-const OP_OTHER_POSTS_READ = 1;
-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',
-};
-
-const OPi18n = {
-  0: 'first_post',
-  1: 'other_posts_read',
-  2: 'other_posts_unread',
-};
-
-// 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)';
-
-function isElementInside(element, outerTag) {
-  while (element !== null && ('tagName' in element)) {
-    if (element.tagName == outerTag) return true;
-    element = element.parentNode;
-  }
-
-  return false;
-}
-
-function escapeUsername(username) {
-  var quoteRegex = /"/g;
-  var commentRegex = /<!---->/g;
-  return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
-}
-
-function getPosts(query, forumId) {
-  return fetch('https://support.google.com/s/community/api/ViewForum', {
-           'credentials': 'include',
-           'headers': {'content-type': 'text/plain; charset=utf-8'},
-           'body': JSON.stringify({
-             '1': forumId,
-             '2': {
-               '1': {
-                 '2': 5,
-               },
-               '2': {
-                 '1': 1,
-                 '2': true,
-               },
-               '12': query,
-             },
-           }),
-           '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 requestId = 0;
-
-  function getMessage(msg) {
-    var id = requestId++;
-
-    return new Promise(function(resolve, reject) {
-      var listener = function(evt) {
-        if (evt.detail.requestId == id) {
-          // Deregister self
-          window.removeEventListener('sendChromeData', listener);
-          resolve(evt.detail.string);
-        }
-      };
-
-      window.addEventListener('sendi18nString', listener);
-
-      var payload = {msg: msg, id: id};
-
-      window.dispatchEvent(new CustomEvent('geti18nString', {detail: payload}));
-    });
-  }
-
-  return {getMessage: getMessage};
-})();
-
-function mutationCallback(mutationList, observer) {
-  mutationList.forEach((mutation) => {
-    if (mutation.type == 'childList') {
-      mutation.addedNodes.forEach(function(node) {
-        if (node.tagName == 'A' && ('href' in node) &&
-            profileRegex.test(node.href) &&
-            isElementInside(node, 'EC-QUESTION') && ('children' in node) &&
-            node.children.length == 0) {
-          var escapedUsername = escapeUsername(node.innerHTML);
-
-          // Create profile indicator dot with a loading state
-          var dotContainer = document.createElement('span');
-          dotContainer.classList.add('profile-indicator');
-          dotContainer.classList.add('profile-indicator--loading');
-          i18nRequest.getMessage('inject_profileindicator_loading')
-              .then(string => dotContainer.setAttribute('title', string));
-
-          var forumUrlSplit = document.location.href.split('/forum/');
-          if (forumUrlSplit.length < 2) throw new Error('Can\'t get forum id.');
-
-          var forumId = forumUrlSplit[1].split('/')[0];
-          var query = '(replier:"' + escapedUsername + '" | creator:"' +
-              escapedUsername + '") ' + FILTER_ALL_LANGUAGES;
-          var encodedQuery = encodeURIComponent(query + ' forum:' + forumId);
-          var urlpart = encodeURIComponent('query=' + encodedQuery);
-          var dotLink = document.createElement('a');
-          dotLink.href =
-              'https://support.google.com/s/community/search/' + urlpart;
-          dotLink.innerText = '●';
-
-          dotContainer.appendChild(dotLink);
-          node.parentNode.appendChild(dotContainer);
-
-          // 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;
-                }
-
-                // Current thread ID
-                var threadUrlSplit = document.location.href.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);
-                }
-
-                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(
-                      'Unexpected error. Couldn\'t load recent posts.', err));
-        }
-      });
-    }
-  });
-};
-
-var observerOptions = {
-  childList: true,
-  subtree: true,
-}
-
-mutationObserver = new MutationObserver(mutationCallback);
-mutationObserver.observe(
-    document.querySelector('.scrollable-content'), observerOptions);
diff --git a/src/injections/console_profileindicator_inject.css b/src/injections/profileindicator_inject.css
similarity index 90%
rename from src/injections/console_profileindicator_inject.css
rename to src/injections/profileindicator_inject.css
index 67b0501..5a71c77 100644
--- a/src/injections/console_profileindicator_inject.css
+++ b/src/injections/profileindicator_inject.css
@@ -1,4 +1,4 @@
-ec-user-link.name > * {
+ec-user-link.name > *, .thread-message-header__name > span:nth-child(3) > * {
   vertical-align: middle;
 }
 
diff --git a/src/injections/profileindicator_inject.js b/src/injections/profileindicator_inject.js
new file mode 100644
index 0000000..ce9187b
--- /dev/null
+++ b/src/injections/profileindicator_inject.js
@@ -0,0 +1,216 @@
+var CCProfileRegex =
+    /^(?:https:\/\/support\.google\.com)?\/s\/community\/forum\/[0-9]*\/user\/(?:[0-9]+)$/;
+var CCRegex = /^https:\/\/support\.google\.com\/s\/community/;
+
+const OP_FIRST_POST = 0;
+const OP_OTHER_POSTS_READ = 1;
+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',
+};
+
+const OPi18n = {
+  0: 'first_post',
+  1: 'other_posts_read',
+  2: 'other_posts_unread',
+};
+
+// 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)';
+
+function isElementInside(element, outerTag) {
+  while (element !== null && ('tagName' in element)) {
+    if (element.tagName == outerTag) return true;
+    element = element.parentNode;
+  }
+
+  return false;
+}
+
+function escapeUsername(username) {
+  var quoteRegex = /"/g;
+  var commentRegex = /<!---->/g;
+  return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
+}
+
+function getPosts(query, forumId) {
+  return fetch('https://support.google.com/s/community/api/ViewForum', {
+           'credentials': 'include',
+           'headers': {'content-type': 'text/plain; charset=utf-8'},
+           'body': JSON.stringify({
+             '1': forumId,
+             '2': {
+               '1': {
+                 '2': 5,
+               },
+               '2': {
+                 '1': 1,
+                 '2': true,
+               },
+               '12': query,
+             },
+           }),
+           '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 requestId = 0;
+
+  function getMessage(msg) {
+    var id = requestId++;
+
+    return new Promise(function(resolve, reject) {
+      var listener = function(evt) {
+        if (evt.detail.requestId == id) {
+          // Deregister self
+          window.removeEventListener('sendChromeData', listener);
+          resolve(evt.detail.string);
+        }
+      };
+
+      window.addEventListener('sendi18nString', listener);
+
+      var payload = {msg: msg, id: id};
+
+      window.dispatchEvent(new CustomEvent('geti18nString', {detail: payload}));
+    });
+  }
+
+  return {getMessage: getMessage};
+})();
+
+// Create profile indicator dot with a loading state
+function createIndicatorDot(sourceNode, searchURL) {
+  var dotContainer = document.createElement('span');
+  dotContainer.classList.add('profile-indicator');
+  dotContainer.classList.add('profile-indicator--loading');
+  i18nRequest.getMessage('inject_profileindicator_loading')
+      .then(string => dotContainer.setAttribute('title', string));
+
+  var dotLink = document.createElement('a');
+  dotLink.href = searchURL;
+  dotLink.innerText = '●';
+
+  dotContainer.appendChild(dotLink);
+  sourceNode.parentNode.appendChild(dotContainer);
+
+  return dotContainer;
+}
+
+// Handle the profile indicator
+function handleIndicatorDot(sourceNode, isCC) {
+  var escapedUsername = escapeUsername(
+      (isCC ? sourceNode.innerHTML :
+              sourceNode.querySelector('span').innerHTML));
+
+  if (isCC) {
+    var threadLink = document.location.href;
+  } else {
+    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.');
+      return;
+    }
+    var threadLink = CCLink.href;
+  }
+
+  var forumUrlSplit = threadLink.split('/forum/');
+  if (forumUrlSplit.length < 2) {
+    console.error('[dotindicator] Can\'t get forum id.');
+    return;
+  }
+
+  var forumId = forumUrlSplit[1].split('/')[0];
+  var query = '(replier:"' + escapedUsername + '" | creator:"' +
+      escapedUsername + '") ' + FILTER_ALL_LANGUAGES;
+  var encodedQuery =
+      encodeURIComponent(query + (isCC ? ' forum:' + forumId : ''));
+  var searchURL =
+      (isCC ? 'https://support.google.com/s/community/search/' +
+               encodeURIComponent('query=' + encodedQuery) :
+              document.location.pathname.split('/thread')[0] +
+               '/threads?thread_filter=' + encodedQuery)
+
+  var dotContainer = createIndicatorDot(sourceNode, searchURL);
+
+  // 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;
+        }
+
+        // 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);
+        }
+
+        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));
+}
+
+if (CCRegex.test(location.href)) {
+  // We are in the Community Console
+  function mutationCallback(mutationList, observer) {
+    mutationList.forEach((mutation) => {
+      if (mutation.type == 'childList') {
+        mutation.addedNodes.forEach(function(node) {
+          if (node.tagName == 'A' && ('href' in node) &&
+              CCProfileRegex.test(node.href) &&
+              isElementInside(node, 'EC-QUESTION') && ('children' in node) &&
+              node.children.length == 0) {
+            handleIndicatorDot(node, true);
+          }
+        });
+      }
+    });
+  };
+
+  var observerOptions = {
+    childList: true,
+    subtree: true,
+  }
+
+  mutationObserver = new MutationObserver(mutationCallback);
+  mutationObserver.observe(
+      document.querySelector('.scrollable-content'), observerOptions);
+} else {
+  // We are in TW
+  var node = document.querySelector('.thread-question .user-info-display-name');
+  if (node !== null)
+    handleIndicatorDot(node, false);
+  else
+    console.error('[dotindicator] Couldn\'t find username.');
+}
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 39d0804..06a9965 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -41,8 +41,8 @@
     "storage"
   ],
   "web_accessible_resources": [
-    "injections/console_profileindicator_inject.js",
-    "injections/console_profileindicator_inject.css"
+    "injections/profileindicator_inject.js",
+    "injections/profileindicator_inject.css"
   ],
   "browser_action": {},
 #if defined(CHROMIUM)