refactor: migrate autorefresh feature to the new architecture

Bug: twpowertools:176
Change-Id: If000d8617e9a4d66bdc7f91c1037867a4cb9fbf2
diff --git a/src/common/architecture/dependenciesProvider/DependenciesProvider.ts b/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
new file mode 100644
index 0000000..132a67c
--- /dev/null
+++ b/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
@@ -0,0 +1,55 @@
+import AutoRefresh from '../../../features/autoRefresh/core/autoRefresh';
+
+export const AutoRefreshDependency = 'autoRefresh';
+export const DependenciesToClass = {
+  [AutoRefreshDependency]: AutoRefresh,
+};
+
+interface OurWindow extends Window {
+  TWPTDependencies?: Dependencies;
+}
+
+type Dependencies = {
+  [K in Dependency]?: InstanceType<(typeof DependenciesToClass)[K]>;
+};
+
+export type Dependency = keyof typeof DependenciesToClass;
+
+class DependenciesProvider {
+  private dependencies: Dependencies;
+
+  constructor() {
+    const ourWindow = window as OurWindow;
+    if (!ourWindow.TWPTDependencies) {
+      ourWindow.TWPTDependencies = {};
+    }
+    this.dependencies = ourWindow.TWPTDependencies;
+  }
+
+  getDependency(dependency: Dependency) {
+    this.setUpDependency(dependency);
+    return this.dependencies[dependency];
+  }
+
+  setUpDependency(dependency: Dependency): void {
+    if (!this.dependencies[dependency]) {
+      this.dependencies[dependency] = new DependenciesToClass[dependency]();
+    }
+  }
+}
+
+export default class DependenciesProviderSingleton {
+  private static instance: DependenciesProvider;
+
+  /**
+   * @see {@link DependenciesProviderSingleton.getInstance}
+   */
+  private constructor() {}
+
+  public static getInstance(): DependenciesProvider {
+    if (!DependenciesProviderSingleton.instance) {
+      DependenciesProviderSingleton.instance = new DependenciesProvider();
+    }
+    return DependenciesProviderSingleton.instance;
+  }
+}
diff --git a/src/common/architecture/scripts/setUpDependencies/SetUpDependenciesScript.ts b/src/common/architecture/scripts/setUpDependencies/SetUpDependenciesScript.ts
new file mode 100644
index 0000000..b5ff6f0
--- /dev/null
+++ b/src/common/architecture/scripts/setUpDependencies/SetUpDependenciesScript.ts
@@ -0,0 +1,13 @@
+import DependenciesProviderSingleton, { Dependency } from "../../dependenciesProvider/DependenciesProvider";
+import Script from "../Script";
+
+export default abstract class SetUpDependenciesScript extends Script {
+  abstract dependencies: Dependency[];
+
+  execute() {
+    const dependenciesProvider = DependenciesProviderSingleton.getInstance();
+    this.dependencies.forEach(dependency => {
+      dependenciesProvider.setUpDependency(dependency);
+    });
+  }
+}
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index ea9cb8c..6b5c7f7 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -154,13 +154,6 @@
       window.TWPTExtraInfo.injectAtExpandedThreadListIfEnabled(node);
     }
 
-    // Set up the autorefresh list feature. The setUp function is responsible
-    // of determining whether it should run or not depending on the current
-    // setting.
-    if (('tagName' in node) && node.tagName == 'EC-THREAD-LIST') {
-      window.TWPTAutoRefresh.setUp();
-    }
-
     if (node.tagName == 'IFRAME') {
       // Redirect unified profile iframe to dark version if applicable
       if (isDarkThemeOn(options) && unifiedProfilesFix.checkIframe(node)) {
@@ -230,11 +223,6 @@
 function handleRemovedNode(mutation, node) {
   if (!('tagName' in node)) return;
 
-  // Remove snackbar when exiting thread list view
-  if (node.tagName == 'EC-THREAD-LIST') {
-    window.TWPTAutoRefresh.hideUpdatePrompt();
-  }
-
   // Readd reply button when the Community Console removes it
   if (node.tagName == 'TWPT-FLATTEN-THREAD-REPLY-BUTTON') {
     flattenThreads.injectReplyBtn(
@@ -271,7 +259,7 @@
   flattenThreads = new FlattenThreads();
   reportDialogColorThemeFix = new ReportDialogColorThemeFix(options);
 
-  // autoRefresh, extraInfo, threadPageDesignWarning and workflowsImport are
+  // extraInfo, threadPageDesignWarning and workflowsImport are
   // initialized in start.js
 
   // Before starting the mutation Observer, check whether we missed any
@@ -325,8 +313,6 @@
   injectStylesheet(chrome.runtime.getURL('css/batchlock_inject.css'));
   // Thread list avatars
   injectStylesheet(chrome.runtime.getURL('css/thread_list_avatars.css'));
-  // Auto refresh list
-  injectStylesheet(chrome.runtime.getURL('css/autorefresh_list.css'));
   // Extra info
   injectStylesheet(chrome.runtime.getURL('css/extrainfo.css'));
   injectStylesheet(chrome.runtime.getURL('css/extrainfo_perforumstats.css'));
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index 71b4779..67142ca 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -4,7 +4,6 @@
 import {getOptions} from '../../common/optionsUtils.js';
 import {kCSTarget, kMWTarget} from '../../xhrInterceptor/responseModifiers/index.js';
 
-import AutoRefresh from './autoRefresh.js';
 import ExtraInfo from './extraInfo/index.js';
 import FlattenThreadsReplyActionHandler from './flattenThreads/replyActionHandler.js';
 import ThreadPageDesignWarning from './threadPageDesignWarning.js';
@@ -60,7 +59,6 @@
 
   // Initialized here instead of in main.js so the first event is received if it
   // happens when the page loads.
-  window.TWPTAutoRefresh = new AutoRefresh();
   window.TWPTExtraInfo = new ExtraInfo();
   window.TWPTThreadPageDesignWarning = new ThreadPageDesignWarning();
   window.TWPTWorkflowsImport = new WorkflowsImport();
diff --git a/src/features/Features.ts b/src/features/Features.ts
index 9b3eaea..98a4b96 100644
--- a/src/features/Features.ts
+++ b/src/features/Features.ts
@@ -4,6 +4,7 @@
   ScriptPage,
   ScriptRunPhase,
 } from '../common/architecture/scripts/Script';
+import AutoRefreshFeature from './autoRefresh/autoRefresh.feature';
 import InfiniteScrollFeature from './infiniteScroll/infiniteScroll.feature';
 
 export type ConcreteFeatureClass = { new (): Feature };
@@ -15,7 +16,10 @@
 }
 
 export default class Features {
-  private features: ConcreteFeatureClass[] = [InfiniteScrollFeature];
+  private features: ConcreteFeatureClass[] = [
+    AutoRefreshFeature,
+    InfiniteScrollFeature,
+  ];
   private initializedFeatures: Feature[];
 
   runScripts(context: Context) {
diff --git a/src/features/autoRefresh/autoRefresh.feature.ts b/src/features/autoRefresh/autoRefresh.feature.ts
new file mode 100644
index 0000000..285690e
--- /dev/null
+++ b/src/features/autoRefresh/autoRefresh.feature.ts
@@ -0,0 +1,16 @@
+import Feature from '../../common/architecture/features/Feature';
+import { ConcreteScript } from '../../common/architecture/scripts/Script';
+import AutoRefreshNodeWatcherScript from './scripts/nodeWatcher.script';
+import AutoRefreshSetUpScript from './scripts/setUp.script';
+import AutoRefreshStylesScript from './scripts/styles.script';
+
+export default class AutoRefreshFeature extends Feature {
+  public readonly scripts: ConcreteScript[] = [
+    AutoRefreshNodeWatcherScript,
+    AutoRefreshSetUpScript,
+    AutoRefreshStylesScript,
+  ];
+
+  readonly codename = 'autoRefresh';
+  readonly relatedOptions = ['autorefresh'];
+}
diff --git a/src/contentScripts/communityConsole/autoRefresh.js b/src/features/autoRefresh/core/autoRefresh.js
similarity index 96%
rename from src/contentScripts/communityConsole/autoRefresh.js
rename to src/features/autoRefresh/core/autoRefresh.js
index 1cbee9f..c2d64d2 100644
--- a/src/contentScripts/communityConsole/autoRefresh.js
+++ b/src/features/autoRefresh/core/autoRefresh.js
@@ -1,11 +1,11 @@
 import {MDCTooltip} from '@material/tooltip';
 
-import {CCApi} from '../../common/api.js';
-import {getAuthUser} from '../../common/communityConsoleUtils.js';
-import {isOptionEnabled} from '../../common/optionsUtils.js';
-import {createPlainTooltip} from '../../common/tooltip.js';
+import {CCApi} from '../../../common/api.js';
+import {getAuthUser} from '../../../common/communityConsoleUtils.js';
+import {isOptionEnabled} from '../../../common/optionsUtils.js';
+import {createPlainTooltip} from '../../../common/tooltip.js';
 
-import {createExtBadge, softRefreshView} from './utils/common.js';
+import {createExtBadge, softRefreshView} from '../../../contentScripts/communityConsole/utils/common.js';
 
 var authuser = getAuthUser();
 
diff --git a/src/features/autoRefresh/nodeWatcherHandlers/threadListHide.handler.ts b/src/features/autoRefresh/nodeWatcherHandlers/threadListHide.handler.ts
new file mode 100644
index 0000000..4b1082d
--- /dev/null
+++ b/src/features/autoRefresh/nodeWatcherHandlers/threadListHide.handler.ts
@@ -0,0 +1,18 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation, NodeMutationType } from '../../../common/nodeWatcher/NodeWatcherHandler';
+import { AutoRefreshNodeWatcherDependencies } from '../scripts/nodeWatcher.script';
+
+/**
+ * Removes the snackbar when exiting thread list view.
+ */
+export default class AutoRefreshThreadListHideHandler extends CssSelectorNodeWatcherScriptHandler<AutoRefreshNodeWatcherDependencies> {
+  cssSelector = 'ec-thread-list';
+
+  readonly mutationTypesProcessed: NodeMutationType[] = [
+    NodeMutationType.RemovedNode,
+  ];
+
+  onMutatedNode(_: NodeMutation) {
+    this.options.autoRefresh.hideUpdatePrompt();
+  }
+}
diff --git a/src/features/autoRefresh/nodeWatcherHandlers/threadListSetUp.handler.ts b/src/features/autoRefresh/nodeWatcherHandlers/threadListSetUp.handler.ts
new file mode 100644
index 0000000..554be00
--- /dev/null
+++ b/src/features/autoRefresh/nodeWatcherHandlers/threadListSetUp.handler.ts
@@ -0,0 +1,14 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../common/nodeWatcher/NodeWatcherHandler';
+import { AutoRefreshNodeWatcherDependencies } from '../scripts/nodeWatcher.script';
+
+/**
+ * Sets up the autorefresh list feature.
+ */
+export default class AutoRefreshThreadListSetUpHandler extends CssSelectorNodeWatcherScriptHandler<AutoRefreshNodeWatcherDependencies> {
+  cssSelector = 'ec-thread-list';
+
+  onMutatedNode(_: NodeMutation) {
+    this.options.autoRefresh.setUp();
+  }
+}
diff --git a/src/features/autoRefresh/scripts/nodeWatcher.script.ts b/src/features/autoRefresh/scripts/nodeWatcher.script.ts
new file mode 100644
index 0000000..5308a35
--- /dev/null
+++ b/src/features/autoRefresh/scripts/nodeWatcher.script.ts
@@ -0,0 +1,33 @@
+import DependenciesProviderSingleton, {
+  AutoRefreshDependency,
+} from '../../../common/architecture/dependenciesProvider/DependenciesProvider';
+import {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../../../common/architecture/scripts/Script';
+import NodeWatcherScript from '../../../common/architecture/scripts/nodeWatcher/NodeWatcherScript';
+import AutoRefresh from '../core/autoRefresh';
+import AutoRefreshThreadListHideHandler from '../nodeWatcherHandlers/threadListHide.handler';
+import AutoRefreshThreadListSetUpHandler from '../nodeWatcherHandlers/threadListSetUp.handler';
+
+export interface AutoRefreshNodeWatcherDependencies {
+  autoRefresh: AutoRefresh;
+}
+
+export default class AutoRefreshNodeWatcherScript extends NodeWatcherScript<AutoRefreshNodeWatcherDependencies> {
+  public page = ScriptPage.CommunityConsole;
+  public environment = ScriptEnvironment.ContentScript;
+  public runPhase = ScriptRunPhase.Main;
+  public handlers = new Map([
+    ['autoRefreshThreadListSetUp', AutoRefreshThreadListSetUpHandler],
+    ['autoRefreshThreadListHide', AutoRefreshThreadListHideHandler],
+  ]);
+
+  protected optionsFactory(): AutoRefreshNodeWatcherDependencies {
+    const dependenciesProvider = DependenciesProviderSingleton.getInstance();
+    return {
+      autoRefresh: dependenciesProvider.getDependency(AutoRefreshDependency),
+    };
+  }
+}
diff --git a/src/features/autoRefresh/scripts/setUp.script.ts b/src/features/autoRefresh/scripts/setUp.script.ts
new file mode 100644
index 0000000..cb5cce6
--- /dev/null
+++ b/src/features/autoRefresh/scripts/setUp.script.ts
@@ -0,0 +1,18 @@
+import {
+  AutoRefreshDependency,
+  Dependency,
+} from '../../../common/architecture/dependenciesProvider/DependenciesProvider';
+import {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../../../common/architecture/scripts/Script';
+import SetUpDependenciesScript from '../../../common/architecture/scripts/setUpDependencies/SetUpDependenciesScript';
+
+export default class AutoRefreshSetUpScript extends SetUpDependenciesScript {
+  public priority = 100;
+  public page = ScriptPage.CommunityConsole;
+  public environment = ScriptEnvironment.ContentScript;
+  public runPhase = ScriptRunPhase.Start;
+  public dependencies: Dependency[] = [AutoRefreshDependency];
+}
diff --git a/src/features/autoRefresh/scripts/styles.script.ts b/src/features/autoRefresh/scripts/styles.script.ts
new file mode 100644
index 0000000..6e78169
--- /dev/null
+++ b/src/features/autoRefresh/scripts/styles.script.ts
@@ -0,0 +1,16 @@
+import Script, {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../../../common/architecture/scripts/Script';
+import { injectStylesheet } from '../../../common/contentScriptsUtils';
+
+export default class AutoRefreshStylesScript extends Script {
+  page = ScriptPage.CommunityConsole;
+  environment = ScriptEnvironment.ContentScript;
+  runPhase = ScriptRunPhase.Main;
+
+  execute() {
+    injectStylesheet(chrome.runtime.getURL('css/autorefresh_list.css'));
+  }
+}
diff --git a/src/platforms/communityConsole/entryPoints/start.ts b/src/platforms/communityConsole/entryPoints/start.ts
new file mode 100644
index 0000000..b7cb6eb
--- /dev/null
+++ b/src/platforms/communityConsole/entryPoints/start.ts
@@ -0,0 +1,16 @@
+import {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../../../common/architecture/scripts/Script';
+import Features from '../../../features/Features';
+
+// Run legacy Javascript entry point
+import '../../../contentScripts/communityConsole/start';
+
+const features = new Features();
+features.runScripts({
+  page: ScriptPage.CommunityConsole,
+  environment: ScriptEnvironment.ContentScript,
+  runPhase: ScriptRunPhase.Start,
+});
diff --git a/webpack.config.js b/webpack.config.js
index a2d0389..06e0b77 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -41,7 +41,7 @@
   let entry = {
     // Content scripts
     communityConsoleMain: './src/platforms/communityConsole/entryPoints/main.ts',
-    communityConsoleStart: './src/contentScripts/communityConsole/start.js',
+    communityConsoleStart: './src/platforms/communityConsole/entryPoints/start.ts',
     publicForum: './src/contentScripts/publicForum.js',
     publicThread: './src/contentScripts/publicThread.js',
     publicThreadStart: './src/contentScripts/publicThreadStart.js',