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/package-lock.json b/package-lock.json
index 47b639c..de77d80 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "@material/mwc-dialog": "^0.27.0",
         "@material/tooltip": "^12.0.0",
         "@material/web": "^0.1.0-alpha.0",
+        "@stdlib/utils-escape-regexp-string": "^0.1.1",
         "async-mutex": "^0.3.2",
         "dompurify": "^2.4.1",
         "google-protobuf": "^3.19.3",
@@ -2433,6 +2434,376 @@
         "@sinonjs/commons": "^3.0.0"
       }
     },
+    "node_modules/@stdlib/assert-has-own-property": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-has-own-property/-/assert-has-own-property-0.1.1.tgz",
+      "integrity": "sha512-Zsylp37i4rz3r0SEknZHDiqRf3StznHRm/tsh4vq6w63C+q8gyFDZVQcwFQ55OrK9OmvWmW+ypAFBUpEFAjX6Q==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/assert-has-symbol-support": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-has-symbol-support/-/assert-has-symbol-support-0.1.1.tgz",
+      "integrity": "sha512-wQpag9EQENPG+EAel6YuUDvNgT8DlaR9s582xE3ScNTLvjNFCzS9M6tFKbgYojxUP7ZCj9VVnh0BAMg+Cxyrww==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/assert-has-tostringtag-support": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-has-tostringtag-support/-/assert-has-tostringtag-support-0.1.1.tgz",
+      "integrity": "sha512-EQbfk5kH0rqaL8fFBPjcj3xloZ+vHMLnSM8TvKS0ViMH2adp/qneR+tr9fk3AavxMikrLe8iK/AdTGGDd5xv9A==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/assert-has-symbol-support": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/assert-is-string": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-is-string/-/assert-is-string-0.1.1.tgz",
+      "integrity": "sha512-GrQjWcirF4xAAp48e4rWpr0SmmUbdsAsoGse9PdOXSLaGRhAjDziLAgx2GLEJMcBuxCR5n1g2dCicPo6bM1hDw==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/assert-has-tostringtag-support": "^0.1.1",
+        "@stdlib/utils-define-nonenumerable-read-only-property": "^0.1.1",
+        "@stdlib/utils-native-class": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/error-tools-fmtprodmsg": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/error-tools-fmtprodmsg/-/error-tools-fmtprodmsg-0.1.1.tgz",
+      "integrity": "sha512-gyQsjgr+TacoDXpPUIkL5LxEx+tT7uWycyv92no0Jo4J+mCzxOq47cBNuwTxboeIKtUSuscD5trWpL9oIdYYbQ==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/types": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/string-base-format-interpolate": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/string-base-format-interpolate/-/string-base-format-interpolate-0.1.1.tgz",
+      "integrity": "sha512-amSiQPOaKNoMgsgvdOCwLjlhrhcj1wUTnBrKmOcjUl3wSR+kRW1zEmGs92DaeGGf0B/SJMe1D3oq5DPriIPP8Q==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/string-base-format-tokenize": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/string-base-format-tokenize/-/string-base-format-tokenize-0.1.1.tgz",
+      "integrity": "sha512-Og33cMtG6btAdA9Dwu69rEZ3/piZEGGLsbt1O2kIi1mxsVk34CO5Wr2JE0VetBjtJ+ylyNm5FODBjsObUhiliQ==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/string-format": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/string-format/-/string-format-0.1.1.tgz",
+      "integrity": "sha512-s/AyPpI2hh2mHcvb/OXrVSv2IQbLlXCL55YSW1HyByVsKRMUgE8RsU2uSuwIIbEczbLIffMugG5ly5QxX7fbCw==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/string-base-format-interpolate": "^0.1.1",
+        "@stdlib/string-base-format-tokenize": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/symbol-ctor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/symbol-ctor/-/symbol-ctor-0.1.1.tgz",
+      "integrity": "sha512-XhhpiWoXwoB6pBkICOlgQsqRbX11TOsA9w18ntv8NAYaimT/dljxuvq258Dq48yuLy/XyFzXYP17uZ92UGFWLg==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/types": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/@stdlib/types/-/types-0.1.0.tgz",
+      "integrity": "sha512-gnUBNBOoDvA5BRJ11e/mNGasMRQwZrTERt6qICAAwqjCJuMamK+SKELv+GNQP/wX6i7rdqhocJy/7Fq7cGbRLQ==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/utils-define-nonenumerable-read-only-property": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-define-nonenumerable-read-only-property/-/utils-define-nonenumerable-read-only-property-0.1.1.tgz",
+      "integrity": "sha512-QCSKAflKO3vuljGaNgFNp3hIbe43CGiXiYET/RPCIo5cjIho8EN0rOQtcbT2s2udiGXXusl0t/v5YLiwueC6JQ==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/types": "^0.1.0",
+        "@stdlib/utils-define-property": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/utils-define-property": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-define-property/-/utils-define-property-0.1.1.tgz",
+      "integrity": "sha512-vy6vaNV5aijnGZi1sWY8GPueOPmGc8No39ejXoSrHymMix83vNsk/pNsY2Usg1J80g71cZIlbCxiJjLJiXmOzw==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/error-tools-fmtprodmsg": "^0.1.0",
+        "@stdlib/string-format": "^0.1.1",
+        "@stdlib/types": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/utils-escape-regexp-string": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-escape-regexp-string/-/utils-escape-regexp-string-0.1.1.tgz",
+      "integrity": "sha512-4eOr5YOA5LjJrNyuaofuXg/lc4qxhpl/ZlazLddtiXWcAqf0nzclFbe/nHvF4fgvgUtYiC9+rRASbZyIDhxxkw==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/assert-is-string": "^0.1.1",
+        "@stdlib/error-tools-fmtprodmsg": "^0.1.0",
+        "@stdlib/string-format": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
+    "node_modules/@stdlib/utils-native-class": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-native-class/-/utils-native-class-0.1.1.tgz",
+      "integrity": "sha512-I8kGJN8T/Fjb+Yew0Asp6bg8Cbb7/XA973UAQ38LCb9uR3uKVsg+mKboMxKrnVkVGvZMp6kfmW8TlowMBzcsrg==",
+      "os": [
+        "aix",
+        "darwin",
+        "freebsd",
+        "linux",
+        "macos",
+        "openbsd",
+        "sunos",
+        "win32",
+        "windows"
+      ],
+      "dependencies": {
+        "@stdlib/assert-has-own-property": "^0.1.1",
+        "@stdlib/assert-has-tostringtag-support": "^0.1.1",
+        "@stdlib/symbol-ctor": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0",
+        "npm": ">2.7.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/stdlib"
+      }
+    },
     "node_modules/@tootallnate/once": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -9849,6 +10220,110 @@
         "@sinonjs/commons": "^3.0.0"
       }
     },
+    "@stdlib/assert-has-own-property": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-has-own-property/-/assert-has-own-property-0.1.1.tgz",
+      "integrity": "sha512-Zsylp37i4rz3r0SEknZHDiqRf3StznHRm/tsh4vq6w63C+q8gyFDZVQcwFQ55OrK9OmvWmW+ypAFBUpEFAjX6Q=="
+    },
+    "@stdlib/assert-has-symbol-support": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-has-symbol-support/-/assert-has-symbol-support-0.1.1.tgz",
+      "integrity": "sha512-wQpag9EQENPG+EAel6YuUDvNgT8DlaR9s582xE3ScNTLvjNFCzS9M6tFKbgYojxUP7ZCj9VVnh0BAMg+Cxyrww=="
+    },
+    "@stdlib/assert-has-tostringtag-support": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-has-tostringtag-support/-/assert-has-tostringtag-support-0.1.1.tgz",
+      "integrity": "sha512-EQbfk5kH0rqaL8fFBPjcj3xloZ+vHMLnSM8TvKS0ViMH2adp/qneR+tr9fk3AavxMikrLe8iK/AdTGGDd5xv9A==",
+      "requires": {
+        "@stdlib/assert-has-symbol-support": "^0.1.1"
+      }
+    },
+    "@stdlib/assert-is-string": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/assert-is-string/-/assert-is-string-0.1.1.tgz",
+      "integrity": "sha512-GrQjWcirF4xAAp48e4rWpr0SmmUbdsAsoGse9PdOXSLaGRhAjDziLAgx2GLEJMcBuxCR5n1g2dCicPo6bM1hDw==",
+      "requires": {
+        "@stdlib/assert-has-tostringtag-support": "^0.1.1",
+        "@stdlib/utils-define-nonenumerable-read-only-property": "^0.1.1",
+        "@stdlib/utils-native-class": "^0.1.1"
+      }
+    },
+    "@stdlib/error-tools-fmtprodmsg": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/error-tools-fmtprodmsg/-/error-tools-fmtprodmsg-0.1.1.tgz",
+      "integrity": "sha512-gyQsjgr+TacoDXpPUIkL5LxEx+tT7uWycyv92no0Jo4J+mCzxOq47cBNuwTxboeIKtUSuscD5trWpL9oIdYYbQ==",
+      "requires": {
+        "@stdlib/types": "^0.1.0"
+      }
+    },
+    "@stdlib/string-base-format-interpolate": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/string-base-format-interpolate/-/string-base-format-interpolate-0.1.1.tgz",
+      "integrity": "sha512-amSiQPOaKNoMgsgvdOCwLjlhrhcj1wUTnBrKmOcjUl3wSR+kRW1zEmGs92DaeGGf0B/SJMe1D3oq5DPriIPP8Q=="
+    },
+    "@stdlib/string-base-format-tokenize": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/string-base-format-tokenize/-/string-base-format-tokenize-0.1.1.tgz",
+      "integrity": "sha512-Og33cMtG6btAdA9Dwu69rEZ3/piZEGGLsbt1O2kIi1mxsVk34CO5Wr2JE0VetBjtJ+ylyNm5FODBjsObUhiliQ=="
+    },
+    "@stdlib/string-format": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/string-format/-/string-format-0.1.1.tgz",
+      "integrity": "sha512-s/AyPpI2hh2mHcvb/OXrVSv2IQbLlXCL55YSW1HyByVsKRMUgE8RsU2uSuwIIbEczbLIffMugG5ly5QxX7fbCw==",
+      "requires": {
+        "@stdlib/string-base-format-interpolate": "^0.1.1",
+        "@stdlib/string-base-format-tokenize": "^0.1.1"
+      }
+    },
+    "@stdlib/symbol-ctor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/symbol-ctor/-/symbol-ctor-0.1.1.tgz",
+      "integrity": "sha512-XhhpiWoXwoB6pBkICOlgQsqRbX11TOsA9w18ntv8NAYaimT/dljxuvq258Dq48yuLy/XyFzXYP17uZ92UGFWLg=="
+    },
+    "@stdlib/types": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/@stdlib/types/-/types-0.1.0.tgz",
+      "integrity": "sha512-gnUBNBOoDvA5BRJ11e/mNGasMRQwZrTERt6qICAAwqjCJuMamK+SKELv+GNQP/wX6i7rdqhocJy/7Fq7cGbRLQ=="
+    },
+    "@stdlib/utils-define-nonenumerable-read-only-property": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-define-nonenumerable-read-only-property/-/utils-define-nonenumerable-read-only-property-0.1.1.tgz",
+      "integrity": "sha512-QCSKAflKO3vuljGaNgFNp3hIbe43CGiXiYET/RPCIo5cjIho8EN0rOQtcbT2s2udiGXXusl0t/v5YLiwueC6JQ==",
+      "requires": {
+        "@stdlib/types": "^0.1.0",
+        "@stdlib/utils-define-property": "^0.1.1"
+      }
+    },
+    "@stdlib/utils-define-property": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-define-property/-/utils-define-property-0.1.1.tgz",
+      "integrity": "sha512-vy6vaNV5aijnGZi1sWY8GPueOPmGc8No39ejXoSrHymMix83vNsk/pNsY2Usg1J80g71cZIlbCxiJjLJiXmOzw==",
+      "requires": {
+        "@stdlib/error-tools-fmtprodmsg": "^0.1.0",
+        "@stdlib/string-format": "^0.1.1",
+        "@stdlib/types": "^0.1.0"
+      }
+    },
+    "@stdlib/utils-escape-regexp-string": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-escape-regexp-string/-/utils-escape-regexp-string-0.1.1.tgz",
+      "integrity": "sha512-4eOr5YOA5LjJrNyuaofuXg/lc4qxhpl/ZlazLddtiXWcAqf0nzclFbe/nHvF4fgvgUtYiC9+rRASbZyIDhxxkw==",
+      "requires": {
+        "@stdlib/assert-is-string": "^0.1.1",
+        "@stdlib/error-tools-fmtprodmsg": "^0.1.0",
+        "@stdlib/string-format": "^0.1.1"
+      }
+    },
+    "@stdlib/utils-native-class": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@stdlib/utils-native-class/-/utils-native-class-0.1.1.tgz",
+      "integrity": "sha512-I8kGJN8T/Fjb+Yew0Asp6bg8Cbb7/XA973UAQ38LCb9uR3uKVsg+mKboMxKrnVkVGvZMp6kfmW8TlowMBzcsrg==",
+      "requires": {
+        "@stdlib/assert-has-own-property": "^0.1.1",
+        "@stdlib/assert-has-tostringtag-support": "^0.1.1",
+        "@stdlib/symbol-ctor": "^0.1.1"
+      }
+    },
     "@tootallnate/once": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
diff --git a/package.json b/package.json
index 675a2ca..444dfde 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
     "@material/mwc-dialog": "^0.27.0",
     "@material/tooltip": "^12.0.0",
     "@material/web": "^0.1.0-alpha.0",
+    "@stdlib/utils-escape-regexp-string": "^0.1.1",
     "async-mutex": "^0.3.2",
     "dompurify": "^2.4.1",
     "google-protobuf": "^3.19.3",
diff --git a/src/common/TWBasicUtils.js b/src/common/TWBasicUtils.js
new file mode 100644
index 0000000..b97fba1
--- /dev/null
+++ b/src/common/TWBasicUtils.js
@@ -0,0 +1,31 @@
+import rescape from '@stdlib/utils-escape-regexp-string';
+
+import {correctArrayKeys} from './protojs';
+
+export function parseView(viewVar) {
+  const escapedViewVar = rescape(viewVar);
+  const viewRegex = new RegExp(`var ${escapedViewVar} ?= ?'([^']+)';`);
+
+  const scripts = document.querySelectorAll('script');
+  let viewData = null;
+  for (let i = 0; i < scripts.length; ++i) {
+    const matches = scripts[i].textContent.match(viewRegex);
+    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);
+      viewData = JSON.parse(rawJson);
+      break;
+    }
+  }
+  if (!viewData) throw new Error(`Could not find ${viewVar} view data.`);
+  return correctArrayKeys(viewData);
+}
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);
 });
diff --git a/src/redirect/index.js b/src/redirect/index.js
new file mode 100644
index 0000000..551d4d6
--- /dev/null
+++ b/src/redirect/index.js
@@ -0,0 +1,66 @@
+import {parseView} from '../common/TWBasicUtils.js';
+import ThreadModel from '../models/Thread.js';
+
+var CCThreadWithoutMessage = /forum\/[0-9]*\/thread\/[0-9]*$/;
+
+/**
+ * @returns {boolean} Whether the redirection was successful.
+ */
+export function redirectIfApplicable() {
+  if (window.TWPTShouldRedirect) {
+    let ok = redirectToCommunityConsoleV2();
+    if (ok) return true;
+
+    return redirectToCommunityConsoleV1();
+  }
+}
+
+function redirectToCommunityConsoleV2() {
+  try {
+    const threadView = parseView('thread_view');
+    if (!threadView) throw new Error('Could not find thread data.');
+
+    const thread = new ThreadModel(threadView);
+    const forumId = thread.getForumId();
+    const threadId = thread.getId();
+
+    if (!forumId || !threadId)
+      throw new Error('Forum id and thread id not present in thread data.');
+
+    const searchParams = new URLSearchParams(location.search);
+    const msgId = searchParams.get('msgid');
+
+    const msgSuffix = msgId ? `/message/${msgId}` : '';
+    const hash = window.TWPTRedirectHash ?? '';
+    const url =
+        `/s/community/forum/${forumId}/thread/${threadId}${msgSuffix}${hash}`;
+    window.location = url;
+    return true;
+  } catch (err) {
+    console.error('Error redirecting to the Community Console (v2): ', err);
+    return false;
+  }
+}
+
+function redirectToCommunityConsoleV1() {
+  try {
+    const redirectLink = document.querySelector('.community-console');
+    if (redirectLink === null) throw new Error('Could not find redirect link.');
+
+    let redirectUrl = redirectLink.href;
+
+    const searchParams = new URLSearchParams(location.search);
+    if (searchParams.has('msgid') && searchParams.get('msgid') !== '' &&
+        CCThreadWithoutMessage.test(redirectUrl)) {
+      redirectUrl +=
+          '/message/' + encodeURIComponent(searchParams.get('msgid'));
+    }
+    redirectUrl += window.TWPTRedirectHash ?? '';
+
+    window.location = redirectUrl;
+    return true;
+  } catch (err) {
+    console.error('Error redirecting to the Community Console (v1): ', err);
+    return false;
+  }
+}
diff --git a/src/redirect/setup.js b/src/redirect/setup.js
new file mode 100644
index 0000000..632bc69
--- /dev/null
+++ b/src/redirect/setup.js
@@ -0,0 +1,16 @@
+import {getOptions} from '../common/optionsUtils.js';
+
+export async function setUpRedirectIfEnabled(options = null) {
+  if (options === null) options = await getOptions(['redirect']);
+
+  if (options.redirect) {
+    setUpRedirect();
+  }
+}
+
+function setUpRedirect() {
+  window.TWPTRedirectHash = window.location.hash;
+  // We're preloading the redirect option so we can redirect faster in
+  // publicThread.js without having to retrieve the options again.
+  window.TWPTShouldRedirect = true;
+}