Add infinite scroll support to CC interop threads

Bug: twpowertools:96
Change-Id: I90eeae09d8bdcb777c69fa4dc2bc0fe8c034cd09
diff --git a/src/contentScripts/communityConsole/infiniteScroll.js b/src/contentScripts/communityConsole/infiniteScroll.js
new file mode 100644
index 0000000..55dc0ae
--- /dev/null
+++ b/src/contentScripts/communityConsole/infiniteScroll.js
@@ -0,0 +1,78 @@
+import {getOptions, isOptionEnabled} from '../../common/optionsUtils.js';
+
+const kInteropLoadMoreClasses = {
+  'scTailwindThreadMorebuttonload-all': 'threadall',
+  'scTailwindThreadMorebuttonload-more': 'thread',
+};
+
+export default class InfiniteScroll {
+  constructor() {
+    this.intersectionObserver = null;
+  }
+
+  setUpIntersectionObserver(node, isScrollableContent) {
+    if (this.intersectionObserver === null) {
+      var scrollableContent = isScrollableContent ?
+          node :
+          node.querySelector('.scrollable-content');
+      if (scrollableContent !== null) {
+        let intersectionOptions = {
+          root: scrollableContent,
+          rootMargin: '0px',
+          threshold: 1.0,
+        };
+        this.intersectionObserver = new IntersectionObserver(
+            this.intersectionCallback, intersectionOptions);
+      }
+    }
+  }
+
+  intersectionCallback(entries, observer) {
+    entries.forEach(entry => {
+      if (entry.isIntersecting) {
+        console.debug('[infinitescroll] Clicking button: ', entry.target);
+        entry.target.click();
+      }
+    });
+  }
+
+  observeLoadMoreBar(bar) {
+    if (this.intersectionObserver === null) {
+      console.warn(
+          '[infinitescroll] ' +
+          'The intersectionObserver is not ready yet.');
+      return;
+    }
+
+    getOptions(['thread', 'threadall']).then(threadOptions => {
+      if (threadOptions.thread)
+        this.intersectionObserver.observe(
+            bar.querySelector('.load-more-button'));
+      if (threadOptions.threadall)
+        this.intersectionObserver.observe(
+            bar.querySelector('.load-all-button'));
+    });
+  }
+
+  observeLoadMoreInteropBtn(btn) {
+    if (this.intersectionObserver === null) {
+      console.warn(
+          '[infinitescroll] ' +
+          'The intersectionObserver is not ready yet.');
+      return;
+    }
+
+    let parentClasses = btn.parentNode?.classList;
+    let feature = null;
+    for (const [c, f] of Object.entries(kInteropLoadMoreClasses)) {
+      if (parentClasses?.contains?.(c)) {
+        feature = f;
+        break;
+      }
+    }
+    if (feature === null) return;
+    isOptionEnabled(feature).then(isEnabled => {
+      if (isEnabled) this.intersectionObserver.observe(btn);
+    });
+  }
+};
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 0ffff41..75de36f 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -8,19 +8,23 @@
 // #!if ['chromium', 'chromium_mv3'].includes(browser_target)
 import {applyDragAndDropFixIfEnabled} from './dragAndDropFix.js';
 // #!endif
+import InfiniteScroll from './infiniteScroll.js';
 import {unifiedProfilesFix} from './unifiedProfiles.js';
 import Workflows from './workflows/workflows.js';
 
-var mutationObserver, intersectionObserver, intersectionOptions, options,
-    avatars, workflows;
+var mutationObserver, options, avatars, infiniteScroll, workflows;
 
 const watchedNodesSelectors = [
   // App container (used to set up the intersection observer and inject the dark
   // mode button)
   'ec-app',
 
-  // Load more bar (for the "load more"/"load all" buttons)
+  // Scrollable content (used for the intersection observer)
+  '.scrollable-content',
+
+  // Load more bar and buttons
   '.load-more-bar',
+  '.scTailwindThreadMorebuttonbutton',
 
   // User profile card inside ec-unified-user
   'ec-unified-user .scTailwindUser_profileUsercardmain',
@@ -63,20 +67,7 @@
 function handleCandidateNode(node) {
   if (typeof node.classList !== 'undefined') {
     if (('tagName' in node) && node.tagName == 'EC-APP') {
-      // Set up the intersectionObserver
-      if (typeof intersectionObserver === 'undefined') {
-        var scrollableContent = node.querySelector('.scrollable-content');
-        if (scrollableContent !== null) {
-          intersectionOptions = {
-            root: scrollableContent,
-            rootMargin: '0px',
-            threshold: 1.0,
-          };
-
-          intersectionObserver = new IntersectionObserver(
-              intersectionCallback, intersectionOptions);
-        }
-      }
+      infiniteScroll.setUpIntersectionObserver(node, false);
 
       // Inject the dark mode button
       // TODO(avm99963): make this feature dynamic.
@@ -87,23 +78,18 @@
       }
     }
 
+    // To set up infinite scroll
+    if (node.classList.contains('scrollable-content')) {
+      infiniteScroll.setUpIntersectionObserver(node, true);
+    }
+
     // Start the intersectionObserver for the "load more"/"load all" buttons
     // inside a thread if the option is currently enabled.
     if (node.classList.contains('load-more-bar')) {
-      if (typeof intersectionObserver !== 'undefined') {
-        getOptions(['thread', 'threadall']).then(threadOptions => {
-          if (threadOptions.thread)
-            intersectionObserver.observe(
-                node.querySelector('.load-more-button'));
-          if (threadOptions.threadall)
-            intersectionObserver.observe(
-                node.querySelector('.load-all-button'));
-        });
-      } else {
-        console.warn(
-            '[infinitescroll] ' +
-            'The intersectionObserver is not ready yet.');
-      }
+      infiniteScroll.observeLoadMoreBar(node);
+    }
+    if (node.classList.contains('scTailwindThreadMorebuttonbutton')) {
+      infiniteScroll.observeLoadMoreInteropBtn(node);
     }
 
     // Show additional details in the profile view.
@@ -213,14 +199,6 @@
   });
 }
 
-function intersectionCallback(entries, observer) {
-  entries.forEach(entry => {
-    if (entry.isIntersecting) {
-      entry.target.click();
-    }
-  });
-};
-
 var observerOptions = {
   childList: true,
   subtree: true,
@@ -231,6 +209,7 @@
 
   // Initialize classes needed by the mutation observer
   avatars = new AvatarsHandler();
+  infiniteScroll = new InfiniteScroll();
   workflows = new Workflows();
 
   // autoRefresh is initialized in start.js