refactor(flatten-threads): migrate to the new DI architecture

Bug: twpowertools:176
Change-Id: I7a24fb504ce53697112f11128d2d5249a5a7c7e7
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 890f3b0..3e914af 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -8,11 +8,10 @@
 // #!if ['chromium', 'chromium_mv3'].includes(browser_target)
 import {applyDragAndDropFixIfEnabled} from './dragAndDropFix.js';
 // #!endif
-import {default as FlattenThreads, kMatchingSelectors as kFlattenThreadMatchingSelectors} from './flattenThreads/flattenThreads.js';
 import {kRepliesSectionSelector} from './threadToolbar/constants.js';
 import ThreadToolbar from './threadToolbar/threadToolbar.js';
 
-var mutationObserver, options, avatars, threadToolbar, flattenThreads;
+var mutationObserver, options, avatars, threadToolbar;
 
 const watchedNodesSelectors = [
   // Scrollable content (used for the intersection observer)
@@ -45,9 +44,6 @@
 
   // Thread page reply section (for the thread page toolbar)
   kRepliesSectionSelector,
-
-  // Reply payload (for the flatten threads UI)
-  ...kFlattenThreadMatchingSelectors,
 ];
 
 function handleCandidateNode(node) {
@@ -94,31 +90,6 @@
     if (threadToolbar.shouldInject(node)) {
       threadToolbar.injectIfApplicable(node);
     }
-
-    // Inject parent reply quote
-    if (flattenThreads.shouldInjectQuote(node)) {
-      flattenThreads.injectQuoteIfApplicable(node);
-    }
-
-    // Inject reply button in non-nested view
-    if (flattenThreads.shouldInjectReplyBtn(node)) {
-      flattenThreads.injectReplyBtnIfApplicable(node);
-    }
-
-    // Delete additional info in the edit message box
-    if (flattenThreads.isAdditionalInfoElement(node)) {
-      flattenThreads.deleteAdditionalInfoElementIfApplicable(node);
-    }
-  }
-}
-
-function handleRemovedNode(mutation, node) {
-  if (!('tagName' in node)) return;
-
-  // Readd reply button when the Community Console removes it
-  if (node.tagName == 'TWPT-FLATTEN-THREAD-REPLY-BUTTON') {
-    flattenThreads.injectReplyBtn(
-        mutation.target, JSON.parse(node.getAttribute('extraInfo')));
   }
 }
 
@@ -128,10 +99,6 @@
       mutation.addedNodes.forEach(function(node) {
         handleCandidateNode(node);
       });
-
-      mutation.removedNodes.forEach(function(node) {
-        handleRemovedNode(mutation, node);
-      });
     }
   });
 }
@@ -147,7 +114,6 @@
   // Initialize classes needed by the mutation observer
   avatars = new AvatarsHandler();
   threadToolbar = new ThreadToolbar();
-  flattenThreads = new FlattenThreads();
 
   // Before starting the mutation Observer, check whether we missed any
   // mutations by manually checking whether some watched nodes already
@@ -202,8 +168,6 @@
   injectStylesheet(chrome.runtime.getURL('css/thread_list_avatars.css'));
   // Thread toolbar
   injectStylesheet(chrome.runtime.getURL('css/thread_toolbar.css'));
-  // Flatten threads
-  injectStylesheet(chrome.runtime.getURL('css/flatten_threads.css'));
 });
 
 new XHRProxyKillSwitchHandler();
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index f890bd0..8cb89e8 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -1,15 +1,9 @@
 import {injectStylesheet} from '../../common/contentScriptsUtils';
 import {getOptions} from '../../common/options/optionsUtils.js';
 
-import FlattenThreadsReplyActionHandler from './flattenThreads/replyActionHandler.js';
-
 getOptions(null).then(options => {
   if (options.uispacing) {
     injectStylesheet(chrome.runtime.getURL('css/ui_spacing/shared.css'));
     injectStylesheet(chrome.runtime.getURL('css/ui_spacing/console.css'));
   }
-
-  const flattenThreadsReplyActionHandler =
-      new FlattenThreadsReplyActionHandler(options);
-  flattenThreadsReplyActionHandler.handleIfApplicable();
 });
diff --git a/src/entryPoints/communityConsole/contentScripts/main.ts b/src/entryPoints/communityConsole/contentScripts/main.ts
index 35088ea..2274fd0 100644
--- a/src/entryPoints/communityConsole/contentScripts/main.ts
+++ b/src/entryPoints/communityConsole/contentScripts/main.ts
@@ -42,6 +42,12 @@
 import InjectLitComponentsScript from '../../../presentation/standaloneScripts/litComponents/injectLitComponents.script';
 import ApplyStartupDataModificationsOnMainScript from '../../../presentation/standaloneScripts/startupDataStorage/applyStartupDataModificationsOnMain.script';
 import ThreadPageDesignWarningInjectHandler from '../../../features/threadPageDesignWarning/presentation/nodeWatcherHandlers/inject.handler';
+import FlattenThreadsAdditionalInfoHandler from '../../../features/flattenThreads/presentation/nodeWatcherHandlers/additionalInfo.handler';
+import FlattenThreadsQuoteHandler from '../../../features/flattenThreads/presentation/nodeWatcherHandlers/quote.handler';
+import FlattenThreadsReaddReplyBtnHandler from '../../../features/flattenThreads/presentation/nodeWatcherHandlers/readdReplyBtn.handler';
+import FlattenThreadsReplyBtnHandler from '../../../features/flattenThreads/presentation/nodeWatcherHandlers/replyBtn.handler';
+import FlattenThreads from '../../../features/flattenThreads/core/flattenThreads';
+import FlattenThreadsStylesScript from '../../../features/flattenThreads/presentation/scripts/styles.script';
 
 const scriptRunner = createScriptRunner();
 scriptRunner.run();
@@ -64,6 +70,7 @@
   );
 
   const ccInfiniteScroll = new CCInfiniteScroll();
+  const flattenThreads = new FlattenThreads();
 
   return new ScriptRunner(
     new SortedScriptsProviderAdapter(
@@ -133,6 +140,22 @@
               new CCInfiniteScrollLoadMoreBtnHandler(ccInfiniteScroll),
             ],
             [
+              'flattenThreadsAdditionalInfo',
+              new FlattenThreadsAdditionalInfoHandler(flattenThreads),
+            ],
+            [
+              'flattenThreadsQuote',
+              new FlattenThreadsQuoteHandler(flattenThreads),
+            ],
+            [
+              'flattenThreadsReaddReplyBtn',
+              new FlattenThreadsReaddReplyBtnHandler(flattenThreads),
+            ],
+            [
+              'flattenThreadsReplyBtn',
+              new FlattenThreadsReplyBtnHandler(flattenThreads),
+            ],
+            [
               'threadPageDesignWarningInject',
               new ThreadPageDesignWarningInjectHandler(threadPageDesignWarning),
             ],
@@ -151,6 +174,7 @@
         new AutoRefreshStylesScript(),
         new CCExtraInfoInjectScript(),
         new CCExtraInfoStylesScript(),
+        new FlattenThreadsStylesScript(),
         new WorkflowsImportStylesheetScript(),
 
         // Standalone scripts
diff --git a/src/entryPoints/communityConsole/contentScripts/start.ts b/src/entryPoints/communityConsole/contentScripts/start.ts
index 72302ff..21ef427 100644
--- a/src/entryPoints/communityConsole/contentScripts/start.ts
+++ b/src/entryPoints/communityConsole/contentScripts/start.ts
@@ -24,6 +24,8 @@
 import ApplyStartupDataModificationsOnStartScript from '../../../presentation/standaloneScripts/startupDataStorage/applyStartupDataModificationsOnStart.script';
 import XHRInterceptorScript from '../../../presentation/standaloneScripts/xhrInterceptor/xhrInterceptor.script';
 import ThreadPageDesignWarningSetUpScript from '../../../features/threadPageDesignWarning/presentation/scripts/setUp.script';
+import FlattenThreadsSetUpReplyActionHandlerScript from '../../../features/flattenThreads/presentation/scripts/setUpReplyActionHandler.script';
+import FlattenThreadsReplyActionHandler from '../../../features/flattenThreads/core/replyActionHandler';
 
 const scriptRunner = createScriptRunner();
 scriptRunner.run();
@@ -53,10 +55,11 @@
         new CCDarkThemeInjectAutoDarkTheme(),
         new CCDarkThemeInjectForcedDarkTheme(),
         new CCExtraInfoSetUpScript(extraInfo),
-        new InteropThreadPageSetupScript(),
-        new ThreadPageDesignWarningSetUpScript(
-          threadPageDesignWarning,
+        new FlattenThreadsSetUpReplyActionHandlerScript(
+          new FlattenThreadsReplyActionHandler(optionsProvider),
         ),
+        new InteropThreadPageSetupScript(),
+        new ThreadPageDesignWarningSetUpScript(threadPageDesignWarning),
         new LoadDraftsSetupScript(optionsProvider, startupDataStorage),
         new WorkflowsImportSetUpScript(workflowsImport),
 
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js b/src/features/flattenThreads/core/components/QuoteAuthor.js
similarity index 100%
rename from src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js
rename to src/features/flattenThreads/core/components/QuoteAuthor.js
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js b/src/features/flattenThreads/core/components/ReplyButton.js
similarity index 96%
rename from src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js
rename to src/features/flattenThreads/core/components/ReplyButton.js
index 8adf676..d54a981 100644
--- a/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js
+++ b/src/features/flattenThreads/core/components/ReplyButton.js
@@ -5,7 +5,7 @@
 
 import {I18nLitElement} from '../../../../common/litI18nUtils.js';
 import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
-import {openReplyEditor} from '../../utils/common.js';
+import {openReplyEditor} from '../../../../contentScripts/communityConsole/utils/common.js';
 import {getExtraInfoNodes} from '../flattenThreads.js';
 
 export default class TwptFlattenThreadReplyButton extends I18nLitElement {
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/index.js b/src/features/flattenThreads/core/components/index.js
similarity index 100%
rename from src/contentScripts/communityConsole/flattenThreads/components/index.js
rename to src/features/flattenThreads/core/components/index.js
diff --git a/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js b/src/features/flattenThreads/core/flattenThreads.js
similarity index 86%
rename from src/contentScripts/communityConsole/flattenThreads/flattenThreads.js
rename to src/features/flattenThreads/core/flattenThreads.js
index 557a533..1e63e24 100644
--- a/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js
+++ b/src/features/flattenThreads/core/flattenThreads.js
@@ -5,11 +5,6 @@
 export const kReplyActionButtonsSelector =
     '.scTailwindThreadMessageMessagecardcontent:not(.scTailwindThreadMessageMessagecardpromoted) sc-tailwind-thread-message-message-actions';
 export const kAdditionalInfoSelector = '.ck-indent-9996300035194';
-export const kMatchingSelectors = [
-  kReplyPayloadSelector,
-  kReplyActionButtonsSelector,
-  kAdditionalInfoSelector,
-];
 
 export function getExtraInfoNodes(node) {
   return node.querySelectorAll(kAdditionalInfoSelector);
@@ -54,10 +49,6 @@
     if (extraInfo.isComment) this.injectQuote(node, extraInfo);
   }
 
-  shouldInjectQuote(node) {
-    return node.matches(kReplyPayloadSelector);
-  }
-
   injectReplyBtnIfApplicable(node) {
     // If we injected the additional information, it means the flatten threads
     // feature is enabled and in actual use, so we should inject the reply
@@ -71,16 +62,8 @@
     this.injectReplyBtn(node, extraInfo);
   }
 
-  shouldInjectReplyBtn(node) {
-    return node.matches(kReplyActionButtonsSelector);
-  }
-
   deleteAdditionalInfoElementIfApplicable(node) {
     if (!node.closest('sc-tailwind-shared-rich-text-editor')) return;
     node.remove();
   }
-
-  isAdditionalInfoElement(node) {
-    return node.matches(kAdditionalInfoSelector);
-  }
 }
diff --git a/src/contentScripts/communityConsole/flattenThreads/replyActionHandler.js b/src/features/flattenThreads/core/replyActionHandler.js
similarity index 78%
rename from src/contentScripts/communityConsole/flattenThreads/replyActionHandler.js
rename to src/features/flattenThreads/core/replyActionHandler.js
index 182f962..3e5d02d 100644
--- a/src/contentScripts/communityConsole/flattenThreads/replyActionHandler.js
+++ b/src/features/flattenThreads/core/replyActionHandler.js
@@ -1,19 +1,14 @@
 import {waitFor} from 'poll-until-promise';
 
 import {parseUrl} from '../../../common/commonUtils';
-import {getOptions} from '../../../common/options/optionsUtils';
 
 const kOpenReplyEditorIntervalInMs = 500;
 const kOpenReplyEditorTimeoutInMs = 10 * 1000;
 
 // @TODO: Handle observing when the hash is added after the page has loaded.
 export default class FlattenThreadsReplyActionHandler {
-  /**
-   * @param {Object} options Options object which at least includes the
-   *     |flattenthreads| and |flattenthreads_switch_enabled| options.
-   */
-  constructor(options = null) {
-    this.options = options;
+  constructor(optionsProvider) {
+    this.optionsProvider = optionsProvider;
   }
 
   async handleIfApplicable() {
@@ -53,13 +48,7 @@
   }
 
   async isFeatureEnabled() {
-    let options;
-    if (this.options !== null) {
-      options = this.options;
-    } else {
-      options =
-          await getOptions(['flattenthreads', 'flattenthreads_switch_enabled']);
-    }
+    const options = await this.optionsProvider.getOptionsValues();
     return options['flattenthreads'] &&
         options['flattenthreads_switch_enabled'];
   }
diff --git a/src/features/flattenThreads/presentation/nodeWatcherHandlers/additionalInfo.handler.ts b/src/features/flattenThreads/presentation/nodeWatcherHandlers/additionalInfo.handler.ts
new file mode 100644
index 0000000..c03b5bd
--- /dev/null
+++ b/src/features/flattenThreads/presentation/nodeWatcherHandlers/additionalInfo.handler.ts
@@ -0,0 +1,18 @@
+import CssSelectorNodeWatcherHandler from '../../../../infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter';
+import { NodeMutation } from '../../../../presentation/nodeWatcher/NodeWatcherHandler';
+import FlattenThreads, {
+  kAdditionalInfoSelector,
+} from '../../core/flattenThreads';
+
+/** Delete additional info in the edit message box */
+export default class FlattenThreadsAdditionalInfoHandler extends CssSelectorNodeWatcherHandler {
+  cssSelector = kAdditionalInfoSelector;
+
+  constructor(private flattenThreads: FlattenThreads) {
+    super();
+  }
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.flattenThreads.deleteAdditionalInfoElementIfApplicable(node);
+  }
+}
diff --git a/src/features/flattenThreads/presentation/nodeWatcherHandlers/quote.handler.ts b/src/features/flattenThreads/presentation/nodeWatcherHandlers/quote.handler.ts
new file mode 100644
index 0000000..c156049
--- /dev/null
+++ b/src/features/flattenThreads/presentation/nodeWatcherHandlers/quote.handler.ts
@@ -0,0 +1,18 @@
+import CssSelectorNodeWatcherHandler from '../../../../infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter';
+import { NodeMutation } from '../../../../presentation/nodeWatcher/NodeWatcherHandler';
+import FlattenThreads, {
+  kReplyPayloadSelector,
+} from '../../core/flattenThreads';
+
+/** Inject parent reply quote */
+export default class FlattenThreadsQuoteHandler extends CssSelectorNodeWatcherHandler {
+  cssSelector = kReplyPayloadSelector;
+
+  constructor(private flattenThreads: FlattenThreads) {
+    super();
+  }
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.flattenThreads.injectQuoteIfApplicable(node);
+  }
+}
diff --git a/src/features/flattenThreads/presentation/nodeWatcherHandlers/readdReplyBtn.handler.ts b/src/features/flattenThreads/presentation/nodeWatcherHandlers/readdReplyBtn.handler.ts
new file mode 100644
index 0000000..2eb3d33
--- /dev/null
+++ b/src/features/flattenThreads/presentation/nodeWatcherHandlers/readdReplyBtn.handler.ts
@@ -0,0 +1,23 @@
+import CssSelectorNodeWatcherHandler from '../../../../infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter';
+import {
+  NodeMutation,
+  NodeMutationType,
+} from '../../../../presentation/nodeWatcher/NodeWatcherHandler';
+import FlattenThreads from '../../core/flattenThreads';
+
+/** Readd reply button when the Community Console removes it */
+export default class FlattenThreadsReaddReplyBtnHandler extends CssSelectorNodeWatcherHandler {
+  cssSelector = 'twpt-flatten-thread-reply-button';
+  mutationTypesProcessed = [NodeMutationType.RemovedNode];
+
+  constructor(private flattenThreads: FlattenThreads) {
+    super();
+  }
+
+  onMutatedNode({ node, mutationRecord }: NodeMutation<HTMLElement>) {
+    this.flattenThreads.injectReplyBtn(
+      mutationRecord.target,
+      JSON.parse(node.getAttribute('extraInfo')),
+    );
+  }
+}
diff --git a/src/features/flattenThreads/presentation/nodeWatcherHandlers/replyBtn.handler.ts b/src/features/flattenThreads/presentation/nodeWatcherHandlers/replyBtn.handler.ts
new file mode 100644
index 0000000..66eff17
--- /dev/null
+++ b/src/features/flattenThreads/presentation/nodeWatcherHandlers/replyBtn.handler.ts
@@ -0,0 +1,18 @@
+import CssSelectorNodeWatcherHandler from '../../../../infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter';
+import { NodeMutation } from '../../../../presentation/nodeWatcher/NodeWatcherHandler';
+import FlattenThreads, {
+  kReplyActionButtonsSelector,
+} from '../../core/flattenThreads';
+
+/** Inject reply button in non-nested view */
+export default class FlattenThreadsReplyBtnHandler extends CssSelectorNodeWatcherHandler {
+  cssSelector = kReplyActionButtonsSelector;
+
+  constructor(private flattenThreads: FlattenThreads) {
+    super();
+  }
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.flattenThreads.injectReplyBtnIfApplicable(node);
+  }
+}
diff --git a/src/features/flattenThreads/presentation/scripts/setUpReplyActionHandler.script.ts b/src/features/flattenThreads/presentation/scripts/setUpReplyActionHandler.script.ts
new file mode 100644
index 0000000..b62140e
--- /dev/null
+++ b/src/features/flattenThreads/presentation/scripts/setUpReplyActionHandler.script.ts
@@ -0,0 +1,20 @@
+import Script from '../../../../common/architecture/scripts/Script';
+import InjectLitComponentsScript from '../../../../presentation/standaloneScripts/litComponents/injectLitComponents.script';
+import FlattenThreadsReplyActionHandler from '../../core/replyActionHandler';
+
+export default class FlattenThreadsSetUpReplyActionHandlerScript extends Script {
+  page: never;
+  environment: never;
+  runPhase: never;
+  runAfter = [InjectLitComponentsScript];
+
+  constructor(
+    private flattenThreadsReplyActionHandler: FlattenThreadsReplyActionHandler,
+  ) {
+    super();
+  }
+
+  execute() {
+    this.flattenThreadsReplyActionHandler.handleIfApplicable();
+  }
+}
diff --git a/src/features/flattenThreads/presentation/scripts/styles.script.ts b/src/features/flattenThreads/presentation/scripts/styles.script.ts
new file mode 100644
index 0000000..338adb2
--- /dev/null
+++ b/src/features/flattenThreads/presentation/scripts/styles.script.ts
@@ -0,0 +1,12 @@
+import Script from '../../../../common/architecture/scripts/Script';
+import { injectStylesheet } from '../../../../common/contentScriptsUtils';
+
+export default class FlattenThreadsStylesScript extends Script {
+  page: never;
+  environment: never;
+  runPhase: never;
+
+  execute() {
+    injectStylesheet(chrome.runtime.getURL('css/flatten_threads.css'));
+  }
+}
diff --git a/src/infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter.ts b/src/infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter.ts
index bf57020..5617130 100644
--- a/src/infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter.ts
+++ b/src/infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter.ts
@@ -28,5 +28,5 @@
     return this.cssSelector;
   }
 
-  abstract onMutatedNode(nodeMutation: NodeMutation): void;
+  abstract onMutatedNode(nodeMutation: NodeMutation<HTMLElement>): void;
 }
diff --git a/src/injections/litComponentsInject.js b/src/injections/litComponentsInject.js
index ce8ace8..7143bd5 100644
--- a/src/injections/litComponentsInject.js
+++ b/src/injections/litComponentsInject.js
@@ -4,7 +4,7 @@
 // because `window.customElements` doesn't exist in content scripts.
 import '../features/workflows/core/communityConsole/components/index.js';
 import '../contentScripts/communityConsole/threadToolbar/components/index.js';
-import '../contentScripts/communityConsole/flattenThreads/components/index.js';
+import '../features/flattenThreads/core/components/index.js';
 import '../contentScripts/communityConsole/updateHandler/banner/components/index.js';
 
 import {injectStylesheet} from '../common/contentScriptsUtils';
diff --git a/src/presentation/nodeWatcher/NodeWatcherHandler.ts b/src/presentation/nodeWatcher/NodeWatcherHandler.ts
index b6c3a25..67801f4 100644
--- a/src/presentation/nodeWatcher/NodeWatcherHandler.ts
+++ b/src/presentation/nodeWatcher/NodeWatcherHandler.ts
@@ -13,11 +13,11 @@
   RemovedNode,
 }
 
-export interface NodeMutation {
+export interface NodeMutation<T extends Node = Node> {
   /**
    * Node being mutated.
    */
-  node: Node;
+  node: T;
   /**
    * Which mutation has occurred to the node.
    */
diff --git a/src/xhrInterceptor/responseModifiers/flattenThread.js b/src/xhrInterceptor/responseModifiers/flattenThread.js
index 2eac64d..452b1fb 100644
--- a/src/xhrInterceptor/responseModifiers/flattenThread.js
+++ b/src/xhrInterceptor/responseModifiers/flattenThread.js
@@ -1,4 +1,4 @@
-import {kAdditionalInfoClass} from '../../contentScripts/communityConsole/flattenThreads/flattenThreads.js';
+import {kAdditionalInfoClass} from '../../features/flattenThreads/core/flattenThreads.js';
 import GapModel from '../../models/Gap.js';
 import MessageModel from '../../models/Message.js';
 import StartupDataModel from '../../models/StartupData.js';