Add per-forum stats to TW profiles

Fixed: twpowertools:102

Change-Id: Ie947c05346a5e430f08b3f3c4fbd13c2c8d744da
diff --git a/src/contentScripts/communityConsole/extraInfo.js b/src/contentScripts/communityConsole/extraInfo.js
index 35572da..af5ac7f 100644
--- a/src/contentScripts/communityConsole/extraInfo.js
+++ b/src/contentScripts/communityConsole/extraInfo.js
@@ -939,7 +939,8 @@
         })
         .then(profile => {
           new PerForumStatsSection(
-              chart?.parentNode, profile.body, this.displayLanguage);
+              chart?.parentNode, profile.body, this.displayLanguage,
+              /* isCommunityConsole = */ true);
         })
         .catch(err => {
           console.error(
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 8c67bb5..f43a25a 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -279,4 +279,5 @@
   injectStylesheet(chrome.runtime.getURL('css/autorefresh_list.css'));
   // Extra info
   injectStylesheet(chrome.runtime.getURL('css/extrainfo.css'));
+  injectStylesheet(chrome.runtime.getURL('css/extrainfo_perforumstats.css'));
 });
diff --git a/src/contentScripts/communityConsole/utils/PerForumStatsSection.js b/src/contentScripts/communityConsole/utils/PerForumStatsSection.js
index 153cfb1..7ca0f38 100644
--- a/src/contentScripts/communityConsole/utils/PerForumStatsSection.js
+++ b/src/contentScripts/communityConsole/utils/PerForumStatsSection.js
@@ -24,8 +24,9 @@
 };
 
 export default class PerForumStatsSection {
-  constructor(existingChartSection, profile, locale) {
+  constructor(existingChartSection, profile, locale, isCommunityConsole) {
     this.locale = locale;
+    this.isCommunityConsole = isCommunityConsole;
     this.parseAndSetData(profile);
     this.buildDOM(existingChartSection);
     if (this.data.length) this.injectChart(this.data[0]?.id);
@@ -42,7 +43,7 @@
 
     this.data = [];
     for (const id of intersectionForumIDs) {
-      const fui = forumUserInfos.find(ui => ui[1] === id)?.[2];
+      const fui = forumUserInfos.find(ui => ui?.[1] === id)?.[2];
       const numMessages = kDataKeys.reduce((prevVal, key) => {
         if (!fui?.[key[0]]) return prevVal;
         return prevVal + fui[key[0]].reduce((prevVal, userActivity) => {
@@ -51,7 +52,7 @@
       }, /* initialValue = */ 0);
       this.data.push({
         id,
-        forumTitle: forumTitles.find(t => t[1] === id)?.[2],
+        forumTitle: forumTitles.find(t => t?.[1] === id)?.[2],
         forumUserInfo: fui,
         numMessages,
       });
@@ -78,9 +79,23 @@
     let title = document.createElement('h2');
     title.classList.add('scTailwindSharedActivitycharttitle');
 
-    const [badge, badgeTooltip] = createExtBadge();
+    let badge, badgeTooltip;
+    if (this.isCommunityConsole) {
+      [badge, badgeTooltip] = createExtBadge();
+    } else {
+      badge = document.createElement('span');
+      badge.classList.add('TWPT-badge');
+
+      var badgeImg = document.createElement('img');
+      badgeImg.src =
+          'https://fonts.gstatic.com/s/i/materialicons/repeat/v6/24px.svg';
+
+      badge.appendChild(badgeImg);
+    }
+
     let titleText = document.createElement('span');
-    titleText.textContent = chrome.i18n.getMessage('inject_perforumstats_heading');
+    titleText.textContent =
+        chrome.i18n.getMessage('inject_perforumstats_heading');
 
     title.append(badge, titleText);
 
@@ -93,7 +108,7 @@
     root.append(title, selector, chartEl);
     section.append(root);
     existingChartSection.after(section);
-    new MDCTooltip(badgeTooltip);
+    if (this.isCommunityConsole) new MDCTooltip(badgeTooltip);
   }
 
   getAplosData(forumId) {
@@ -101,10 +116,15 @@
     for (const [key, name, color] of kDataKeys) {
       let rawData = this.data.find(f => f.id === forumId)?.forumUserInfo?.[key];
       let data;
-      if (!rawData)
+      if (!rawData) {
         data = [];
-      else
-        data = rawData.map(m => JSON.stringify(Object.values(m)));
+      } else {
+        // We're filtering empty strings since in the public forum there a lose
+        // conversion takes place and the first element of the array is always
+        // null, which breaks the Aplos graph rendering.
+        data =
+            rawData.map(m => JSON.stringify(Object.values(m))).filter(m => !!m);
+      }
       aplosData.push({
         color,
         data,
diff --git a/src/contentScripts/profile.js b/src/contentScripts/profile.js
deleted file mode 100644
index 3f7a3fa..0000000
--- a/src/contentScripts/profile.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import {getOptions} from '../common/optionsUtils.js';
-
-import {injectPreviousPostsLinksUnifiedProfile} from './utilsCommon/unifiedProfiles.js';
-
-getOptions('history').then(options => {
-  if (options?.history)
-    injectPreviousPostsLinksUnifiedProfile(/* isCommunityConsole = */ false);
-});
diff --git a/src/contentScripts/publicProfile.js b/src/contentScripts/publicProfile.js
new file mode 100644
index 0000000..fec0c85
--- /dev/null
+++ b/src/contentScripts/publicProfile.js
@@ -0,0 +1,43 @@
+import {getOptions} from '../common/optionsUtils.js';
+
+import PerForumStatsSection from './communityConsole/utils/PerForumStatsSection.js';
+import {correctArrayKeys} from './utilsCommon/protojs.js';
+import {injectPreviousPostsLinksUnifiedProfile} from './utilsCommon/unifiedProfiles.js';
+
+const profileViewRegex = /var view ?= ?(.+\]);/;
+
+getOptions(['history', 'extrainfo']).then(options => {
+  if (options?.history)
+    injectPreviousPostsLinksUnifiedProfile(/* isCommunityConsole = */ false);
+
+  if (options?.extrainfo) {
+    try {
+      // Find chart
+      const chart = document.querySelector(
+          'sc-tailwind-user_profile-user-profile ' +
+          '.scTailwindUser_profileUserprofilesection ' +
+          'sc-tailwind-shared-activity-chart');
+      if (!chart) throw new Error('Couldn\'t find existing chart.');
+
+      // Extract profile JSON information
+      const scripts = document.querySelectorAll('script');
+      let profileView = null;
+      for (let i = 0; i < scripts.length; ++i) {
+        const matches = scripts[i].textContent.match(profileViewRegex);
+        if (matches?.[1]) {
+          profileView = JSON.parse(matches[1]);
+          break;
+        }
+      }
+      const profileViewC = {'1': correctArrayKeys(profileView)};
+      console.log(profileViewC);
+      if (!profileView) throw new Error('Could not find user view data.');
+      new PerForumStatsSection(
+          chart?.parentNode, profileViewC,
+          document.documentElement?.lang ?? 'en',
+          /* isCommunityConsole = */ false);
+    } catch (err) {
+      console.error('Error while injecting extra info: ', err);
+    }
+  }
+});
diff --git a/src/contentScripts/publicProfileStart.js b/src/contentScripts/publicProfileStart.js
new file mode 100644
index 0000000..f5a3a2f
--- /dev/null
+++ b/src/contentScripts/publicProfileStart.js
@@ -0,0 +1,7 @@
+import {injectScript} from '../common/contentScriptsUtils.js';
+import {getOptions} from '../common/optionsUtils.js';
+
+getOptions('extrainfo').then(options => {
+  if (options?.extrainfo)
+    injectScript(chrome.runtime.getURL('extraInfoInject.bundle.js'));
+});
diff --git a/src/contentScripts/utilsCommon/protojs.js b/src/contentScripts/utilsCommon/protojs.js
new file mode 100644
index 0000000..026559b
--- /dev/null
+++ b/src/contentScripts/utilsCommon/protojs.js
@@ -0,0 +1,13 @@
+// Function which converts a protobuf array into an array which can be accessed
+// as if it was a protobuf object (with the same keys). If the input is not an
+// array it returns itself (since it is called recursively).
+export function correctArrayKeys(input) {
+  if (!Array.isArray(input)) return input;
+
+  let object = [];
+  for (let i = 0; i < input.length; ++i) {
+    if (input[i] === null) continue;
+    object[i + 1] = correctArrayKeys(input[i]);
+  }
+  return object;
+}
diff --git a/src/static/_locales/en/messages.json b/src/static/_locales/en/messages.json
index 492ab4d..f6ac4be 100644
--- a/src/static/_locales/en/messages.json
+++ b/src/static/_locales/en/messages.json
@@ -156,7 +156,7 @@
     "description": "Feature checkbox in the options page"
   },
   "options_perforumstats": {
-    "message": "Show per-forum activity in profiles in the Community Console.",
+    "message": "Show per-forum activity in profiles.",
     "description": "Feature checkbox in the options page"
   },
   "options_save": {
diff --git a/src/static/css/extrainfo.css b/src/static/css/extrainfo.css
index eeb8694..ae15f8d 100644
--- a/src/static/css/extrainfo.css
+++ b/src/static/css/extrainfo.css
@@ -126,28 +126,3 @@
   --icon-size: 9.5px;
   margin-right: 2px;
 }
-
-
-/* Per-forum stats section */
-.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitychartchart {
-  margin-top: 26px;
-}
-
-.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitycharttitle {
-  display: flex;
-  align-items: center;
-}
-
-.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitycharttitle .TWPT-badge {
-  margin-inline-end: 6px;
-}
-
-.TWPT-scTailwindSharedActivitychartroot .TWPT-select-container {
-  position: absolute;
-  top: 56px;
-  z-index: 2;
-}
-
-.TWPT-scTailwindSharedActivitychartroot .TWPT-select-container select {
-  max-width: 300px;
-}
diff --git a/src/static/css/extrainfo_perforumstats.css b/src/static/css/extrainfo_perforumstats.css
new file mode 100644
index 0000000..ff73e98
--- /dev/null
+++ b/src/static/css/extrainfo_perforumstats.css
@@ -0,0 +1,23 @@
+/* Per-forum stats section */
+.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitychartchart {
+  margin-top: 26px;
+}
+
+.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitycharttitle {
+  display: flex;
+  align-items: center;
+}
+
+.TWPT-scTailwindSharedActivitychartroot .scTailwindSharedActivitycharttitle .TWPT-badge {
+  margin-inline-end: 6px;
+}
+
+.TWPT-scTailwindSharedActivitychartroot .TWPT-select-container {
+  position: absolute;
+  top: 56px;
+  z-index: 2;
+}
+
+.TWPT-scTailwindSharedActivitychartroot .TWPT-select-container select {
+  max-width: 300px;
+}
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 2524612..b4f0281 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -44,8 +44,14 @@
     {
       "matches": ["https://support.google.com/*/profile/*", "https://support.google.com/profile/*"],
       "all_frames": true,
-      "js": ["profile.bundle.js", "mdcStyles.bundle.js"],
-      "css": ["css/common/forum.css", "css/unifiedprofile.css"]
+      "js": ["publicProfileStart.bundle.js"],
+      "run_at": "document_start"
+    },
+    {
+      "matches": ["https://support.google.com/*/profile/*", "https://support.google.com/profile/*"],
+      "all_frames": true,
+      "js": ["publicProfile.bundle.js", "mdcStyles.bundle.js"],
+      "css": ["css/common/forum.css", "css/unifiedprofile.css", "css/extrainfo_perforumstats.css"]
     }
   ],
   "permissions": [
@@ -82,6 +88,7 @@
         "css/autorefresh_list.css",
         "css/image_max_height.css",
         "css/extrainfo.css",
+        "css/extrainfo_perforumstats.css",
 
         "communityConsoleMain.bundle.js.map",
         "communityConsoleStart.bundle.js.map",
diff --git a/webpack.config.js b/webpack.config.js
index 6024eb4..9d28abb 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -43,7 +43,8 @@
     communityConsoleStart: './src/contentScripts/communityConsole/start.js',
     publicForum: './src/contentScripts/publicForum.js',
     publicThread: './src/contentScripts/publicThread.js',
-    profile: './src/contentScripts/profile.js',
+    publicProfile: './src/contentScripts/publicProfile.js',
+    publicProfileStart: './src/contentScripts/publicProfileStart.js',
     profileIndicator: './src/contentScripts/profileIndicator.js',
 
     // Injected JS