refactor: convert CCInfiniteScroll class to Typescript

Change-Id: I4ec59b96a71b1a23a16f3d5dd923e9d8fd3f2638
diff --git a/src/features/infiniteScroll/core/ccInfiniteScroll.js b/src/features/infiniteScroll/core/ccInfiniteScroll.js
deleted file mode 100644
index 4dde91c..0000000
--- a/src/features/infiniteScroll/core/ccInfiniteScroll.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import {getOptions, isOptionEnabled} from '../../../common/options/optionsUtils.js';
-
-const kInteropLoadMoreClasses = {
-  // New (interop) UI without nested replies
-  'scTailwindThreadMorebuttonload-all': 'threadall',
-  'scTailwindThreadMorebuttonload-more': 'thread',
-
-  // New (interop) UI with nested replies
-  'scTailwindThreadMessagegapload-all': 'threadall',
-  'scTailwindThreadMessagegapload-more': 'thread',
-};
-const kArtificialScrollingDelay = 3500;
-
-export default class CCInfiniteScroll {
-  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();
-      }
-    });
-  }
-
-  isPotentiallyArtificialScroll() {
-    return window.location.href.includes('/message/');
-  }
-
-  observeWithPotentialDelay(node) {
-    if (this.intersectionObserver === null) {
-      console.warn(
-          '[infinitescroll] ' +
-          'The intersectionObserver is not ready yet.');
-      return;
-    }
-
-    if (this.isPotentiallyArtificialScroll()) {
-      window.setTimeout(
-          () => {this.intersectionObserver.observe(node)},
-          kArtificialScrollingDelay);
-    } else {
-      this.intersectionObserver.observe(node);
-    }
-  }
-
-  observeLoadMoreBar(bar) {
-    getOptions(['thread', 'threadall']).then(threadOptions => {
-      if (threadOptions.thread)
-        this.observeWithPotentialDelay(bar.querySelector('.load-more-button'));
-      if (threadOptions.threadall)
-        this.observeWithPotentialDelay(bar.querySelector('.load-all-button'));
-    });
-  }
-
-  observeLoadMoreInteropBtn(btn) {
-    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.observeWithPotentialDelay(btn);
-    });
-  }
-};
diff --git a/src/features/infiniteScroll/core/ccInfiniteScroll.ts b/src/features/infiniteScroll/core/ccInfiniteScroll.ts
new file mode 100644
index 0000000..84c3d63
--- /dev/null
+++ b/src/features/infiniteScroll/core/ccInfiniteScroll.ts
@@ -0,0 +1,96 @@
+import {
+  getOptions,
+  isOptionEnabled,
+} from '../../../common/options/optionsUtils.js';
+
+const kInteropLoadMoreClasses = {
+  // New (interop) UI without nested replies
+  'scTailwindThreadMorebuttonload-all': 'threadall',
+  'scTailwindThreadMorebuttonload-more': 'thread',
+
+  // New (interop) UI with nested replies
+  'scTailwindThreadMessagegapload-all': 'threadall',
+  'scTailwindThreadMessagegapload-more': 'thread',
+};
+const kArtificialScrollingDelay = 3500;
+
+export default class CCInfiniteScroll {
+  private intersectionObserver: IntersectionObserver = null;
+
+  setUpIntersectionObserver(node: Element, isScrollableContent: boolean) {
+    if (this.intersectionObserver === null) {
+      const scrollableContent = isScrollableContent
+        ? node
+        : node.querySelector('.scrollable-content');
+      if (scrollableContent !== null) {
+        const intersectionOptions = {
+          root: scrollableContent,
+          rootMargin: '0px',
+          threshold: 1.0,
+        };
+        this.intersectionObserver = new IntersectionObserver(
+          this.intersectionCallback,
+          intersectionOptions,
+        );
+      }
+    }
+  }
+
+  intersectionCallback(entries: IntersectionObserverEntry[]) {
+    entries.forEach((entry) => {
+      if (entry.isIntersecting) {
+        const target = entry.target;
+        if (!(target instanceof HTMLElement)) return;
+        console.debug('[infinitescroll] Clicking button: ', target);
+        target.click();
+      }
+    });
+  }
+
+  isPotentiallyArtificialScroll(): boolean {
+    return window.location.href.includes('/message/');
+  }
+
+  observeWithPotentialDelay(node: Element) {
+    if (this.intersectionObserver === null) {
+      console.warn(
+        '[infinitescroll] The intersectionObserver is not ready yet.',
+      );
+      return;
+    }
+
+    if (this.isPotentiallyArtificialScroll()) {
+      window.setTimeout(() => {
+        this.intersectionObserver.observe(node);
+      }, kArtificialScrollingDelay);
+    } else {
+      this.intersectionObserver.observe(node);
+    }
+  }
+
+  observeLoadMoreBar(bar: Element) {
+    getOptions(['thread', 'threadall']).then((threadOptions) => {
+      if (threadOptions.thread)
+        this.observeWithPotentialDelay(bar.querySelector('.load-more-button'));
+      if (threadOptions.threadall)
+        this.observeWithPotentialDelay(bar.querySelector('.load-all-button'));
+    });
+  }
+
+  observeLoadMoreInteropBtn(btn: Element) {
+    const parentNode = btn.parentNode;
+    if (!(parentNode instanceof Element)) return;
+    const parentClasses = parentNode?.classList;
+    let feature: string = 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.observeWithPotentialDelay(btn);
+    });
+  }
+}
diff --git a/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler.ts b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler.ts
index da4f5e6..e9f04ff 100644
--- a/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler.ts
+++ b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler.ts
@@ -6,6 +6,13 @@
   cssSelector = '.load-more-bar';
 
   onMutatedNode({ node }: NodeMutation) {
+    if (!(node instanceof Element)) {
+      console.error(
+        '[CCInfiniteScrollLoadMoreBarHandler] Node is not an Element: ',
+        node,
+      );
+      return;
+    }
     this.options.ccInfiniteScroll.observeLoadMoreBar(node);
   }
 }
diff --git a/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler.ts b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler.ts
index ee54aff..8bf6fd2 100644
--- a/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler.ts
+++ b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler.ts
@@ -7,6 +7,13 @@
     '.scTailwindThreadMorebuttonbutton, .scTailwindThreadMessagegapbutton';
 
   onMutatedNode({ node }: NodeMutation) {
+    if (!(node instanceof Element)) {
+      console.error(
+        '[CCInfiniteScrollLoadMoreBtnHandler] Node is not an Element: ',
+        node,
+      );
+      return;
+    }
     this.options.ccInfiniteScroll.observeLoadMoreInteropBtn(node);
   }
 }