refactor(cc-dark-theme): migrate to the new DI architecture

Bug: twpowertools:226
Change-Id: I735013393d1d99cadee48399bba53a22fe59e27c
diff --git a/src/common/architecture/dependenciesProvider/DependenciesProvider.ts b/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
index 794a681..4f01b82 100644
--- a/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
+++ b/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
@@ -1,6 +1,6 @@
 import ExtraInfo from '../../../features/extraInfo/core';
 import AutoRefresh from '../../../features/autoRefresh/core/autoRefresh';
-import OptionsProvider from '../../options/OptionsProvider';
+import OptionsProviderAdapter from '../../options/OptionsProvider';
 import WorkflowsImport from '../../../features/workflows/core/communityConsole/import';
 import Workflows from '../../../features/workflows/core/communityConsole/workflows';
 import StartupDataStorage from '../../../contentScripts/communityConsole/utils/StartupDataStorage';
@@ -17,7 +17,7 @@
 export const DependenciesToClass = {
   [AutoRefreshDependency]: AutoRefresh,
   [ExtraInfoDependency]: ExtraInfo,
-  [OptionsProviderDependency]: OptionsProvider,
+  [OptionsProviderDependency]: OptionsProviderAdapter,
   [ReportDialogColorThemeFixDependency]: ReportDialogColorThemeFix,
   [StartupDataStorageDependency]: StartupDataStorage,
   [WorkflowsDependency]: Workflows,
diff --git a/src/common/architecture/scripts/stylesheet/StylesheetScript.ts b/src/common/architecture/scripts/stylesheet/StylesheetScript.ts
index 991afa5..2ed9025 100644
--- a/src/common/architecture/scripts/stylesheet/StylesheetScript.ts
+++ b/src/common/architecture/scripts/stylesheet/StylesheetScript.ts
@@ -1,6 +1,6 @@
 import StylesheetManager from '../../../StylesheetManager';
 import { StylesheetAttributes } from '../../../contentScriptsUtils';
-import OptionsProvider from '../../../options/OptionsProvider';
+import OptionsProviderAdapter from '../../../options/OptionsProvider';
 import DependenciesProviderSingleton, {
   OptionsProviderDependency,
 } from '../../dependenciesProvider/DependenciesProvider';
@@ -23,11 +23,13 @@
    */
   readonly attributes: StylesheetAttributes = {};
 
-  protected optionsProvider: OptionsProvider;
+  protected optionsProvider: OptionsProviderAdapter;
   private stylesheetManager: StylesheetManager;
 
   constructor() {
     super();
+
+    // TODO(https://iavm.xyz/b/226): Retrieve this via constructor injection.
     const dependenciesProvider = DependenciesProviderSingleton.getInstance();
     this.optionsProvider = dependenciesProvider.getDependency(
       OptionsProviderDependency,
diff --git a/src/common/options/OptionsProvider.ts b/src/common/options/OptionsProvider.ts
index 138da07..94a43de 100644
--- a/src/common/options/OptionsProvider.ts
+++ b/src/common/options/OptionsProvider.ts
@@ -3,6 +3,10 @@
 import { getOptions } from './optionsUtils';
 import { OptionCodename, OptionsValues } from './optionsPrototype';
 import { OptionsConfiguration } from './OptionsConfiguration';
+import {
+  OptionsChangeListener,
+  OptionsProviderPort,
+} from '../../services/options/OptionsProvider';
 
 // Prioritize reads before writes.
 const kReadPriority = 10;
@@ -11,12 +15,46 @@
 /**
  * Class which provides option values and a way to listen to option changes.
  */
-export default class OptionsProvider {
+export default class OptionsProviderAdapter implements OptionsProviderPort {
   private optionsConfiguration: OptionsConfiguration;
   private mutex: MutexInterface = withTimeout(new Mutex(), 60 * 1000);
   private listeners: Set<OptionsChangeListener> = new Set();
+  private isSetUp = false;
 
-  constructor() {
+  async getOptionValue<O extends OptionCodename>(
+    option: O,
+  ): Promise<OptionsValues[O]> {
+    this.setUp();
+    return this.mutex.runExclusive(
+      () => this.optionsConfiguration.getOptionValue(option),
+      kReadPriority,
+    );
+  }
+
+  async isEnabled(option: OptionCodename): Promise<boolean> {
+    this.setUp();
+    return this.mutex.runExclusive(
+      () => this.optionsConfiguration.isEnabled(option),
+      kReadPriority,
+    );
+  }
+
+  async getOptionsValues(): Promise<OptionsValues> {
+    this.setUp();
+    return this.mutex.runExclusive(
+      () => this.optionsConfiguration.optionsValues,
+      kReadPriority,
+    );
+  }
+
+  addListener(listener: OptionsChangeListener) {
+    this.setUp();
+    this.listeners.add(listener);
+  }
+
+  private setUp() {
+    if (this.isSetUp) return;
+
     this.listenForStorageChanges();
     this.updateValues();
   }
@@ -66,45 +104,4 @@
       listener(previousOptionsConfiguration, this.optionsConfiguration);
     }
   }
-
-  /**
-   * Returns the value of option |option|.
-   */
-  async getOptionValue<O extends OptionCodename>(
-    option: O,
-  ): Promise<OptionsValues[O]> {
-    return this.mutex.runExclusive(
-      () => this.optionsConfiguration.getOptionValue(option),
-      kReadPriority,
-    );
-  }
-
-  /**
-   * Returns whether |feature| is enabled.
-   */
-  async isEnabled(option: OptionCodename): Promise<boolean> {
-    return this.mutex.runExclusive(
-      () => this.optionsConfiguration.isEnabled(option),
-      kReadPriority,
-    );
-  }
-
-  async getOptionsValues(): Promise<OptionsValues> {
-    return this.mutex.runExclusive(
-      () => this.optionsConfiguration.optionsValues,
-      kReadPriority,
-    );
-  }
-
-  /**
-   * Adds a listener for changes in the options configuration.
-   */
-  addListener(listener: OptionsChangeListener) {
-    this.listeners.add(listener);
-  }
 }
-
-export type OptionsChangeListener = (
-  previousOptionValues: OptionsConfiguration,
-  currentOptionValues: OptionsConfiguration,
-) => void;
diff --git a/src/entryPoints/communityConsole/contentScripts/main.ts b/src/entryPoints/communityConsole/contentScripts/main.ts
index ff70c11..fb6fa17 100644
--- a/src/entryPoints/communityConsole/contentScripts/main.ts
+++ b/src/entryPoints/communityConsole/contentScripts/main.ts
@@ -1,5 +1,6 @@
 import DependenciesProviderSingleton, {
   AutoRefreshDependency,
+  OptionsProviderDependency,
 } from '../../../common/architecture/dependenciesProvider/DependenciesProvider';
 import { Context } from '../../../common/architecture/entrypoint/Context';
 import {
@@ -21,6 +22,10 @@
 
 // Run legacy Javascript entry point
 import '../../../contentScripts/communityConsole/main';
+import CCDarkThemeEcAppHandler from '../../../features/ccDarkTheme/nodeWatcherHandlers/ecApp.handler';
+import CCDarkThemeReportDialogHandler from '../../../features/ccDarkTheme/nodeWatcherHandlers/reportDialog.handler';
+import CCDarkThemeUnifiedProfilesIframeHandler from '../../../features/ccDarkTheme/nodeWatcherHandlers/unifiedProfilesIframe.handler';
+import ReportDialogColorThemeFix from '../../../features/ccDarkTheme/core/logic/reportDialog';
 
 const scriptRunner = createScriptRunner();
 scriptRunner.run();
@@ -28,6 +33,9 @@
 function createScriptRunner() {
   const dependenciesProvider = DependenciesProviderSingleton.getInstance();
   const autoRefresh = dependenciesProvider.getDependency(AutoRefreshDependency);
+  const optionsProvider = dependenciesProvider.getDependency(
+    OptionsProviderDependency,
+  );
 
   const context: Context = {
     page: ScriptPage.CommunityConsole,
@@ -50,6 +58,18 @@
               'autoRefreshThreadListHide',
               new AutoRefreshThreadListHideHandler(autoRefresh),
             ],
+            ['ccDarkThemeEcApp', new CCDarkThemeEcAppHandler(optionsProvider)],
+            [
+              'ccDarkThemeReportDialog',
+              new CCDarkThemeReportDialogHandler(
+                optionsProvider,
+                new ReportDialogColorThemeFix(),
+              ),
+            ],
+            [
+              'ccDarkThemeUnifiedProfilesIframe',
+              new CCDarkThemeUnifiedProfilesIframeHandler(optionsProvider),
+            ],
           ]),
         ),
 
diff --git a/src/entryPoints/communityConsole/contentScripts/start.ts b/src/entryPoints/communityConsole/contentScripts/start.ts
index 5e11a87..e9be92c 100644
--- a/src/entryPoints/communityConsole/contentScripts/start.ts
+++ b/src/entryPoints/communityConsole/contentScripts/start.ts
@@ -16,6 +16,8 @@
 
 // Run legacy Javascript entry point
 import '../../../contentScripts/communityConsole/start';
+import CCDarkThemeInjectAutoDarkTheme from '../../../features/ccDarkTheme/scripts/injectAutoDarkTheme.script';
+import CCDarkThemeInjectForcedDarkTheme from '../../../features/ccDarkTheme/scripts/injectForcedDarkTheme.script';
 
 const scriptRunner = createScriptRunner();
 scriptRunner.run();
@@ -35,6 +37,8 @@
       [
         // Individual feature scripts
         new AutoRefreshSetUpScript(autoRefresh),
+        new CCDarkThemeInjectAutoDarkTheme(),
+        new CCDarkThemeInjectForcedDarkTheme(),
 
         // Non-DI scripts (legacy, should be migrated to use a DI approach)
         ...new Features().getScripts(context),
diff --git a/src/features/Features.ts b/src/features/Features.ts
index bc09eee..6bd74b0 100644
--- a/src/features/Features.ts
+++ b/src/features/Features.ts
@@ -3,7 +3,6 @@
 import ScriptFilterListProvider from '../common/architecture/scripts/ScriptFilterListProvider';
 import ExtraInfoFeature from './extraInfo/extraInfo.feature';
 import WorkflowsFeature from './workflows/workflows.feature';
-import CCDarkThemeFeature from './ccDarkTheme/ccDarkTheme.feature';
 import LoadDraftsFeature from './loadDrafts/loadDrafts.feature';
 import InteropThreadPageFeature from './interopThreadPage/interopThreadPage.feature';
 
@@ -11,7 +10,6 @@
 
 export default class Features extends ScriptFilterListProvider {
   private features: ConcreteFeatureClass[] = [
-    CCDarkThemeFeature,
     ExtraInfoFeature,
     InfiniteScrollFeature,
     InteropThreadPageFeature,
diff --git a/src/features/ccDarkTheme/ccDarkTheme.feature.ts b/src/features/ccDarkTheme/ccDarkTheme.feature.ts
deleted file mode 100644
index 74efbf2..0000000
--- a/src/features/ccDarkTheme/ccDarkTheme.feature.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import Feature from '../../common/architecture/features/Feature';
-import { ConcreteScript } from '../../common/architecture/scripts/Script';
-import { OptionCodename } from '../../common/options/optionsPrototype';
-import InjectAutoDarkTheme from './scripts/injectAutoDarkTheme.script';
-import InjectForcedDarkTheme from './scripts/injectForcedDarkTheme.script';
-import CCDarkThemeNodeWatcherScript from './scripts/nodeWatcher.script';
-
-export default class CCDarkThemeFeature extends Feature {
-  public readonly scripts: ConcreteScript[] = [
-    InjectAutoDarkTheme,
-    InjectForcedDarkTheme,
-    CCDarkThemeNodeWatcherScript,
-  ];
-
-  readonly codename = 'darkTheme';
-  readonly relatedOptions: OptionCodename[] = [
-    'ccdarktheme',
-    'ccdarktheme_mode',
-    'ccdarktheme_switch_status',
-  ];
-}
diff --git a/src/features/ccDarkTheme/nodeWatcherHandlers/ecApp.handler.ts b/src/features/ccDarkTheme/nodeWatcherHandlers/ecApp.handler.ts
index fee1ffd..355de3f 100644
--- a/src/features/ccDarkTheme/nodeWatcherHandlers/ecApp.handler.ts
+++ b/src/features/ccDarkTheme/nodeWatcherHandlers/ecApp.handler.ts
@@ -1,20 +1,23 @@
-import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import CssSelectorNodeWatcherHandler from '../../../infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter';
 import { NodeMutation } from '../../../presentation/nodeWatcher/NodeWatcherHandler';
+import { OptionsProviderPort } from '../../../services/options/OptionsProvider';
 import { injectDarkThemeButton } from '../core/logic/darkTheme';
-import { CCDarkThemeNodeWatcherDependencies } from '../scripts/nodeWatcher.script';
 
 /**
  * Injects the dark theme button.
  */
-export default class CCDarkThemeEcAppHandler extends CssSelectorNodeWatcherScriptHandler<CCDarkThemeNodeWatcherDependencies> {
+export default class CCDarkThemeEcAppHandler extends CssSelectorNodeWatcherHandler {
   cssSelector = 'ec-app';
 
+  constructor(private optionsProvider: OptionsProviderPort) {
+    super();
+  }
+
   async onMutatedNode(mutation: NodeMutation) {
     if (!(mutation.node instanceof Element)) return;
 
-    const optionsProvider = this.options.optionsProvider;
-    const isEnabled = await optionsProvider.isEnabled('ccdarktheme');
-    const mode = await optionsProvider.getOptionValue('ccdarktheme_mode');
+    const isEnabled = await this.optionsProvider.isEnabled('ccdarktheme');
+    const mode = await this.optionsProvider.getOptionValue('ccdarktheme_mode');
 
     // TODO(avm99963): make this feature dynamic.
     if (isEnabled && mode === 'switch') {
diff --git a/src/features/ccDarkTheme/nodeWatcherHandlers/reportDialog.handler.ts b/src/features/ccDarkTheme/nodeWatcherHandlers/reportDialog.handler.ts
index 42c7a59..56699e9 100644
--- a/src/features/ccDarkTheme/nodeWatcherHandlers/reportDialog.handler.ts
+++ b/src/features/ccDarkTheme/nodeWatcherHandlers/reportDialog.handler.ts
@@ -1,17 +1,25 @@
-import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import CssSelectorNodeWatcherHandler from '../../../infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter';
 import { NodeMutation } from '../../../presentation/nodeWatcher/NodeWatcherHandler';
-import { CCDarkThemeNodeWatcherDependencies } from '../scripts/nodeWatcher.script';
+import { OptionsProviderPort } from '../../../services/options/OptionsProvider';
+import ReportDialogColorThemeFix from '../core/logic/reportDialog';
 
 /**
  * Sets the report dialog iframe's theme to the appropriate theme.
  */
-export default class CCDarkThemeReportDialogHandler extends CssSelectorNodeWatcherScriptHandler<CCDarkThemeNodeWatcherDependencies> {
+export default class CCDarkThemeReportDialogHandler extends CssSelectorNodeWatcherHandler {
   cssSelector = 'iframe';
 
+  constructor(
+    private optionsProvider: OptionsProviderPort,
+    private reportDialogColorThemeFix: ReportDialogColorThemeFix,
+  ) {
+    super();
+  }
+
   onMutatedNode(mutation: NodeMutation) {
-    this.options.reportDialogColorThemeFix.fixThemeIfReportDialogIframeAndApplicable(
+    this.reportDialogColorThemeFix.fixThemeIfReportDialogIframeAndApplicable(
       mutation.node,
-      this.options.optionsProvider,
+      this.optionsProvider,
     );
   }
 }
diff --git a/src/features/ccDarkTheme/nodeWatcherHandlers/unifiedProfilesIframe.handler.ts b/src/features/ccDarkTheme/nodeWatcherHandlers/unifiedProfilesIframe.handler.ts
index 6343834..28b9753 100644
--- a/src/features/ccDarkTheme/nodeWatcherHandlers/unifiedProfilesIframe.handler.ts
+++ b/src/features/ccDarkTheme/nodeWatcherHandlers/unifiedProfilesIframe.handler.ts
@@ -1,17 +1,21 @@
-import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import CssSelectorNodeWatcherHandler from '../../../infrastructure/presentation/nodeWatcher/handlers/CssSelectorHandler.adapter';
 import { NodeMutation } from '../../../presentation/nodeWatcher/NodeWatcherHandler';
+import { OptionsProviderPort } from '../../../services/options/OptionsProvider';
 import { isDarkThemeOn } from '../core/logic/darkTheme';
 import { unifiedProfilesFix } from '../core/logic/unifiedProfiles';
-import { CCDarkThemeNodeWatcherDependencies } from '../scripts/nodeWatcher.script';
 
 /**
  * Redirect unified profile iframe to dark version if applicable
  */
-export default class CCDarkThemeUnifiedProfilesIframeHandler extends CssSelectorNodeWatcherScriptHandler<CCDarkThemeNodeWatcherDependencies> {
+export default class CCDarkThemeUnifiedProfilesIframeHandler extends CssSelectorNodeWatcherHandler {
   cssSelector = 'iframe';
 
+  constructor(private optionsProvider: OptionsProviderPort) {
+    super();
+  }
+
   async onMutatedNode(mutation: NodeMutation) {
-    const optionsValues = await this.options.optionsProvider.getOptionsValues();
+    const optionsValues = await this.optionsProvider.getOptionsValues();
 
     if (
       isDarkThemeOn(optionsValues) &&
diff --git a/src/features/ccDarkTheme/scripts/injectAutoDarkTheme.script.ts b/src/features/ccDarkTheme/scripts/injectAutoDarkTheme.script.ts
index c462a03..c1bf0a1 100644
--- a/src/features/ccDarkTheme/scripts/injectAutoDarkTheme.script.ts
+++ b/src/features/ccDarkTheme/scripts/injectAutoDarkTheme.script.ts
@@ -1,7 +1,7 @@
 import { ScriptPage } from '../../../common/architecture/scripts/Script';
 import StylesheetScript from '../../../common/architecture/scripts/stylesheet/StylesheetScript';
 
-export default class InjectAutoDarkTheme extends StylesheetScript {
+export default class CCDarkThemeInjectAutoDarkTheme extends StylesheetScript {
   stylesheet = 'ccDarkTheme.bundle.css';
   attributes = { media: '(prefers-color-scheme: dark)' };
   page = ScriptPage.CommunityConsole;
diff --git a/src/features/ccDarkTheme/scripts/injectForcedDarkTheme.script.ts b/src/features/ccDarkTheme/scripts/injectForcedDarkTheme.script.ts
index ebd221b..04c624a 100644
--- a/src/features/ccDarkTheme/scripts/injectForcedDarkTheme.script.ts
+++ b/src/features/ccDarkTheme/scripts/injectForcedDarkTheme.script.ts
@@ -1,7 +1,7 @@
 import { ScriptPage } from '../../../common/architecture/scripts/Script';
 import StylesheetScript from '../../../common/architecture/scripts/stylesheet/StylesheetScript';
 
-export default class InjectForcedDarkTheme extends StylesheetScript {
+export default class CCDarkThemeInjectForcedDarkTheme extends StylesheetScript {
   stylesheet = 'ccDarkTheme.bundle.css';
   page = ScriptPage.CommunityConsole;
 
diff --git a/src/features/ccDarkTheme/scripts/nodeWatcher.script.ts b/src/features/ccDarkTheme/scripts/nodeWatcher.script.ts
deleted file mode 100644
index 83e343e..0000000
--- a/src/features/ccDarkTheme/scripts/nodeWatcher.script.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import DependenciesProviderSingleton, {
-  OptionsProviderDependency,
-  ReportDialogColorThemeFixDependency,
-} from '../../../common/architecture/dependenciesProvider/DependenciesProvider';
-import {
-  ScriptEnvironment,
-  ScriptPage,
-  ScriptRunPhase,
-} from '../../../common/architecture/scripts/Script';
-import LegacyNodeWatcherScript from '../../../common/architecture/scripts/nodeWatcher/LegacyNodeWatcherScript';
-import OptionsProvider from '../../../common/options/OptionsProvider';
-import ReportDialogColorThemeFix from '../core/logic/reportDialog';
-import CCDarkThemeEcAppHandler from '../nodeWatcherHandlers/ecApp.handler';
-import CCDarkThemeReportDialogHandler from '../nodeWatcherHandlers/reportDialog.handler';
-import CCDarkThemeUnifiedProfilesIframeHandler from '../nodeWatcherHandlers/unifiedProfilesIframe.handler';
-
-export interface CCDarkThemeNodeWatcherDependencies {
-  reportDialogColorThemeFix: ReportDialogColorThemeFix;
-  optionsProvider: OptionsProvider;
-}
-
-export default class CCDarkThemeNodeWatcherScript extends LegacyNodeWatcherScript<CCDarkThemeNodeWatcherDependencies> {
-  public page = ScriptPage.CommunityConsole;
-  public environment = ScriptEnvironment.ContentScript;
-  public runPhase = ScriptRunPhase.Main;
-  public handlers = new Map([
-    ['cc-dark-theme-ec-app', CCDarkThemeEcAppHandler],
-    ['cc-dark-theme-report-dialog', CCDarkThemeReportDialogHandler],
-    [
-      'cc-dark-theme-unified-profiles-iframe',
-      CCDarkThemeUnifiedProfilesIframeHandler,
-    ],
-  ]);
-
-  protected optionsFactory(): CCDarkThemeNodeWatcherDependencies {
-    const dependenciesProvider = DependenciesProviderSingleton.getInstance();
-    return {
-      reportDialogColorThemeFix: dependenciesProvider.getDependency(
-        ReportDialogColorThemeFixDependency,
-      ),
-      optionsProvider: dependenciesProvider.getDependency(
-        OptionsProviderDependency,
-      ),
-    };
-  }
-}
diff --git a/src/services/options/OptionsProvider.ts b/src/services/options/OptionsProvider.ts
new file mode 100644
index 0000000..2e64156
--- /dev/null
+++ b/src/services/options/OptionsProvider.ts
@@ -0,0 +1,31 @@
+import { OptionsConfiguration } from '../../common/options/OptionsConfiguration';
+import {
+  OptionCodename,
+  OptionsValues,
+} from '../../common/options/optionsPrototype';
+
+export interface OptionsProviderPort {
+  /**
+   * Returns the value of option |option|.
+   */
+  getOptionValue<O extends OptionCodename>(
+    option: O,
+  ): Promise<OptionsValues[O]>;
+
+  /**
+   * Returns whether |feature| is enabled.
+   */
+  isEnabled(option: OptionCodename): Promise<boolean>;
+
+  getOptionsValues(): Promise<OptionsValues>;
+
+  /**
+   * Adds a listener for changes in the options configuration.
+   */
+  addListener(listener: OptionsChangeListener): void;
+}
+
+export type OptionsChangeListener = (
+  previousOptionValues: OptionsConfiguration,
+  currentOptionValues: OptionsConfiguration,
+) => void;