feat(cc-redirect): redirect URL hash and add redundant redirect method

This CL adds logic to redirect the URL hash to the Community Console,
so actions that are embedded in the URL like |#action=reply| will be
passed to the Community Console.

It also adds another method of redirecting to the Community Console
which will coexist with the old method.

Fixed: twpowertools:164
Change-Id: Ib3f770d7cbeec8f26cdd249e66f7f46ae94bb1c8
diff --git a/src/contentScripts/publicProfile.js b/src/contentScripts/publicProfile.js
index 6c3da8d..9c36ae1 100644
--- a/src/contentScripts/publicProfile.js
+++ b/src/contentScripts/publicProfile.js
@@ -1,10 +1,10 @@
 import {getOptions} from '../common/optionsUtils.js';
-import {correctArrayKeys} from '../common/protojs.js';
+import {parseView} from '../common/TWBasicUtils.js';
 
 import PerForumStatsSection from './communityConsole/utils/PerForumStatsSection.js';
 import {injectPreviousPostsLinksUnifiedProfile} from './utilsCommon/unifiedProfiles.js';
 
-const profileViewRegex = /var view ?= ?'([^']+)';/;
+const kProfileViewVar = 'view';
 
 getOptions(['history', 'perforumstats']).then(options => {
   if (options?.history)
@@ -20,27 +20,8 @@
       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]) {
-          let rawJsonStringContents =
-              matches[1]
-                  .replace(
-                      /\\x([0-9a-f]{2})/ig,
-                      (_, pair) => {
-                        return String.fromCharCode(parseInt(pair, 16));
-                      })
-                  .replace(/\\'/g, `'`)
-                  .replace(/"/g, `\\"`);
-          let rawJsonString = `"${rawJsonStringContents}"`;
-          let rawJson = JSON.parse(rawJsonString);
-          profileView = JSON.parse(rawJson);
-          break;
-        }
-      }
-      const profileViewC = {'1': correctArrayKeys(profileView)};
+      const profileView = parseView(kProfileViewVar);
+      const profileViewC = {'1': profileView};
       if (!profileView) throw new Error('Could not find user view data.');
       new PerForumStatsSection(
           chart?.parentNode, profileViewC,
diff --git a/src/contentScripts/publicThread.js b/src/contentScripts/publicThread.js
index dd2088a..4d98bee 100644
--- a/src/contentScripts/publicThread.js
+++ b/src/contentScripts/publicThread.js
@@ -1,7 +1,6 @@
 import {injectStylesheet} from '../common/contentScriptsUtils.js';
 import {getOptions} from '../common/optionsUtils.js';
-
-var CCThreadWithoutMessage = /forum\/[0-9]*\/thread\/[0-9]*$/;
+import {redirectIfApplicable} from '../redirect/index.js';
 
 const kLoadMoreButtons = [
   {
@@ -23,15 +22,27 @@
 ];
 const kMsgidDelay = 3500;
 
+function main() {
+  const ok = redirectIfApplicable();
+  if (ok) return;
+
+  getOptions(null).then(options => {
+    setUpInfiniteScrollWithPotentialDelay(options);
+
+    if (options.imagemaxheight)
+      injectStylesheet(chrome.runtime.getURL('css/image_max_height.css'));
+  });
+}
+
 var intersectionObserver;
 
-function intersectionCallback(entries, observer) {
+function intersectionCallback(entries) {
   entries.forEach(entry => {
     if (entry.isIntersecting) {
       entry.target.click();
     }
   });
-};
+}
 
 var intersectionOptions = {
   threshold: 1.0,
@@ -65,22 +76,4 @@
   }
 }
 
-getOptions(null).then(options => {
-  var redirectLink = document.querySelector('.community-console');
-  if (options.redirect && redirectLink !== null) {
-    var redirectUrl = redirectLink.href;
-
-    var searchParams = new URLSearchParams(location.search);
-    if (searchParams.has('msgid') && searchParams.get('msgid') !== '' &&
-        CCThreadWithoutMessage.test(redirectUrl))
-      redirectUrl +=
-          '/message/' + encodeURIComponent(searchParams.get('msgid'));
-
-    window.location = redirectUrl;
-  } else {
-    setUpInfiniteScrollWithPotentialDelay(options);
-
-    if (options.imagemaxheight)
-      injectStylesheet(chrome.runtime.getURL('css/image_max_height.css'));
-  }
-});
+main();
diff --git a/src/contentScripts/publicThreadStart.js b/src/contentScripts/publicThreadStart.js
index d1c4e3c..9581aec 100644
--- a/src/contentScripts/publicThreadStart.js
+++ b/src/contentScripts/publicThreadStart.js
@@ -1,9 +1,12 @@
 import {injectStylesheet} from '../common/contentScriptsUtils.js';
-import {isOptionEnabled} from '../common/optionsUtils.js';
+import {getOptions} from '../common/optionsUtils.js';
+import {setUpRedirectIfEnabled} from '../redirect/setup.js';
 
-isOptionEnabled('uispacing').then(isUiSpacingEnabled => {
-  if (!isUiSpacingEnabled) return;
+getOptions(['uispacing', 'redirect']).then(options => {
+  if (options.uispacing) {
+    injectStylesheet(chrome.runtime.getURL('css/ui_spacing/shared.css'));
+    injectStylesheet(chrome.runtime.getURL('css/ui_spacing/twbasic.css'));
+  }
 
-  injectStylesheet(chrome.runtime.getURL('css/ui_spacing/shared.css'));
-  injectStylesheet(chrome.runtime.getURL('css/ui_spacing/twbasic.css'));
+  setUpRedirectIfEnabled(options);
 });