Make nested replies warning in old UI smarter

The warning shown in the old thread UI when the nested replies feature
is enabled in a thread has been improved. Before, it only worked by
checking whether the thread was posted in one of the few forums we knew
nested replies were enabled there. These forums were manually hardcoded
into the extension.

However, now the extension detects whether the thread shown is using
nested replies by inspecting the ViewThread response, just like TW does.

Bug: twpowertools:139
Change-Id: I3470eb600f743f14ca565151c19f333f741c544e
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index bc7988d..34b0f1d 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -9,10 +9,9 @@
 import {applyDragAndDropFixIfEnabled} from './dragAndDropFix.js';
 // #!endif
 import InfiniteScroll from './infiniteScroll.js';
-import ThreadPageDesignWarning from './threadPageDesignWarning.js';
 import {unifiedProfilesFix} from './unifiedProfiles.js';
 
-var mutationObserver, options, avatars, infiniteScroll, threadPageDesignWarning;
+var mutationObserver, options, avatars, infiniteScroll;
 
 const watchedNodesSelectors = [
   // App container (used to set up the intersection observer and inject the dark
@@ -189,8 +188,9 @@
       window.TWPTExtraInfo.injectPerForumStatsIfEnabled(node);
     }
 
-    if (node.matches('ec-thread > .page > .material-content > div[role="list"]')) {
-      threadPageDesignWarning.injectWarningIfApplicable(node);
+    if (node.matches(
+            'ec-thread > .page > .material-content > div[role="list"]')) {
+      window.TWPTThreadPageDesignWarning.injectWarningIfApplicable(node);
     }
   }
 }
@@ -227,9 +227,9 @@
   // Initialize classes needed by the mutation observer
   avatars = new AvatarsHandler();
   infiniteScroll = new InfiniteScroll();
-  threadPageDesignWarning = new ThreadPageDesignWarning();
 
-  // autoRefresh and extraInfo are initialized in start.js
+  // autoRefresh, extraInfo and threadPageDesignWarning are initialized in
+  // start.js
 
   // Before starting the mutation Observer, check whether we missed any
   // mutations by manually checking whether some watched nodes already
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index 02995e7..f0c43d8 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -3,6 +3,7 @@
 
 import AutoRefresh from './autoRefresh.js';
 import ExtraInfo from './extraInfo.js';
+import ThreadPageDesignWarning from './threadPageDesignWarning.js';
 
 const SMEI_NESTED_REPLIES = 15;
 const SMEI_RCE_THREAD_INTEROP = 22;
@@ -48,6 +49,7 @@
   // happens when the page loads.
   window.TWPTAutoRefresh = new AutoRefresh();
   window.TWPTExtraInfo = new ExtraInfo();
+  window.TWPTThreadPageDesignWarning = new ThreadPageDesignWarning();
 
   if (options.ccdarktheme) {
     switch (options.ccdarktheme_mode) {
diff --git a/src/contentScripts/communityConsole/threadPageDesignWarning.js b/src/contentScripts/communityConsole/threadPageDesignWarning.js
index ae26938..f58112e 100644
--- a/src/contentScripts/communityConsole/threadPageDesignWarning.js
+++ b/src/contentScripts/communityConsole/threadPageDesignWarning.js
@@ -8,11 +8,18 @@
 
 import {createExtBadge} from './utils/common.js';
 
-// Forums where Nested Replies have been enabled
-const NRS_ENABLED_FORUM_IDS = [51488989];
+const kSMEINestedReplies = 15;
+const kViewThreadResponse = 'TWPT_ViewThreadResponse';
 
 export default class ThreadPageDesignWarning {
   constructor() {
+    this.lastThread = {
+      body: {},
+      id: -1,
+      timestamp: 0,
+    };
+    this.setUpHandler();
+
     // We have to save whether the old UI was enabled at startup, since that's
     // the moment when it takes effect. If the option changes while the tab is
     // open, it won't take effect.
@@ -25,8 +32,39 @@
       if (this.shouldShowWarning) {
         injectStylesheet(
             chrome.runtime.getURL('css/thread_page_design_warning.css'));
+      } else {
+        this.removeHandler();
       }
     });
+
+    this.isExperimentEnabled = this.isNestedRepliesExperimentEnabled();
+  }
+
+  isNestedRepliesExperimentEnabled() {
+    if (!document.documentElement.hasAttribute('data-startup')) return false;
+
+    let startup =
+        JSON.parse(document.documentElement.getAttribute('data-startup'));
+    return startup?.[1]?.[6]?.includes?.(kSMEINestedReplies);
+  }
+
+  eventHandler(e) {
+    if (e.detail.id < this.lastThread.id) return;
+
+    this.lastThread = {
+      body: e.detail.body,
+      id: e.detail.id,
+      timestamp: Date.now(),
+    };
+  }
+
+  setUpHandler() {
+    window.addEventListener(kViewThreadResponse, this.eventHandler.bind(this));
+  }
+
+  removeHandler() {
+    window.removeEventListener(
+        kViewThreadResponse, this.eventHandler.bind(this));
   }
 
   injectWarning(content) {
@@ -76,21 +114,44 @@
                    return Promise.reject(
                        new Error('shouldShowWarning is not defined.'));
 
-                 return Promise.resolve(this.shouldShowWarning);
+                 return Promise.resolve({result: this.shouldShowWarning});
                },
                {
                  interval: 500,
                  timeout: 10 * 1000,
                })
+        .then(preShouldShowWarning => {
+          if (!preShouldShowWarning.result) return;
+
+          // If the global SMEI experiment is enabled, all threads use nested
+          // replies, so we'll skip the per-thread check and always show the
+          // warning banner.
+          if (this.isExperimentEnabled) return Promise.resolve(true);
+
+          let currentThread = parseUrl(location.href);
+          if (currentThread === false)
+            throw new Error('current thread id cannot be parsed.');
+
+          return waitFor(() => {
+            let now = Date.now();
+            let lastThreadInfo = this.lastThread.body['1']?.['2']?.['1'];
+            if (now - this.lastThread.timestamp > 30 * 1000 ||
+                lastThreadInfo?.['1'] != currentThread.thread ||
+                lastThreadInfo?.['3'] != currentThread.forum)
+              throw new Error(
+                  'cannot obtain information about current thread.');
+
+            // If the messageOrGap field contains any items, the thread is using
+            // nested replies. Otherwise, it probably isn't using them.
+            return Promise.resolve(
+                {result: this.lastThread.body['1']?.['40']?.length > 0});
+          }, {
+            interval: 500,
+            timeout: 10 * 1000,
+          });
+        })
         .then(shouldShowWarning => {
-          if (!shouldShowWarning) return;
-
-          let thread = parseUrl(location.href);
-          if (thread === false)
-            throw new Error('current thread cannot be parsed.');
-
-          if (NRS_ENABLED_FORUM_IDS.includes(parseInt(thread.forum)))
-            this.injectWarning(content);
+          if (shouldShowWarning.result) this.injectWarning(content);
         })
         .catch(err => {
           console.error(