refactor: migrate extra info feature to the new architecture

Bug: twpowertools:176
Change-Id: I379216066b973fe76f000ab9581053c1f0da569e
diff --git a/src/common/architecture/dependenciesProvider/DependenciesProvider.ts b/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
index 132a67c..b6af2a1 100644
--- a/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
+++ b/src/common/architecture/dependenciesProvider/DependenciesProvider.ts
@@ -1,16 +1,23 @@
+import ExtraInfo from '../../../features/extraInfo/core';
 import AutoRefresh from '../../../features/autoRefresh/core/autoRefresh';
 
 export const AutoRefreshDependency = 'autoRefresh';
+export const ExtraInfoDependency = 'extraInfo';
 export const DependenciesToClass = {
   [AutoRefreshDependency]: AutoRefresh,
+  [ExtraInfoDependency]: ExtraInfo,
 };
 
 interface OurWindow extends Window {
   TWPTDependencies?: Dependencies;
 }
 
+export type ClassFromDependency<T extends Dependency> = InstanceType<
+  (typeof DependenciesToClass)[T]
+>;
+
 type Dependencies = {
-  [K in Dependency]?: InstanceType<(typeof DependenciesToClass)[K]>;
+  [K in Dependency]?: ClassFromDependency<K>;
 };
 
 export type Dependency = keyof typeof DependenciesToClass;
@@ -26,14 +33,22 @@
     this.dependencies = ourWindow.TWPTDependencies;
   }
 
-  getDependency(dependency: Dependency) {
+  /**
+   * Gets an instance of a dependency, and creates it beforehand if it doesn't exist yet.
+   */
+  getDependency<T extends Dependency>(dependency: T): ClassFromDependency<T> {
     this.setUpDependency(dependency);
-    return this.dependencies[dependency];
+    const dep = this.dependencies[dependency];
+    if (!dep) {
+      throw new Error(`Dependency ${dependency} not found.`);
+    }
+    return dep;
   }
 
-  setUpDependency(dependency: Dependency): void {
+  setUpDependency<T extends Dependency>(dependency: T): void {
     if (!this.dependencies[dependency]) {
-      this.dependencies[dependency] = new DependenciesToClass[dependency]();
+      const dependencyClass = DependenciesToClass[dependency];
+      this.dependencies[dependency] = new dependencyClass() as Dependencies[T];
     }
   }
 }
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 6b5c7f7..637e061 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -46,12 +46,9 @@
   'ec-bulk-actions material-button[debugid="mark-read-button"]',
   'ec-bulk-actions material-button[debugid="mark-unread-button"]',
 
-  // Thread list items (used to inject the avatars and extra info)
+  // Thread list items (used to inject the avatars)
   'li',
 
-  // Thread list item toolbelt (used for the extra info feature)
-  'ec-thread-summary .main .toolbelt',
-
   // Thread list (used for the autorefresh feature)
   'ec-thread-list',
 
@@ -61,19 +58,6 @@
   // Canned response tags (for the "import CR" popup for the workflows feature)
   'ec-canned-response-row .tags',
 
-  // Question state chips container (for the extra info feature)
-  'sc-tailwind-thread-question-question-card sc-tailwind-thread-question-state-chips',
-
-  // Replies (for the extra info feature)
-  'sc-tailwind-thread-message-message-list sc-tailwind-thread-message-message-card',
-
-  // Comments (for the extra info feature)
-  'sc-tailwind-thread-message-message-list sc-tailwind-thread-message-comment-card',
-
-  // User activity chart (for the per-forum stats feature)
-  'ec-unified-user .scTailwindUser_profileUserprofilesection ' +
-      'sc-tailwind-shared-activity-chart',
-
   // Thread page main content
   'ec-thread > .page > .material-content > div[role="list"]',
 
@@ -97,11 +81,6 @@
       }
     }
 
-    // Show additional details in the profile view.
-    if (node.matches('ec-unified-user .scTailwindUser_profileUsercardmain')) {
-      window.TWPTExtraInfo.injectAbuseChipsAtProfileIfEnabled(node);
-    }
-
     // Show the "previous posts" links if the option is currently enabled.
     //   Here we're selecting the 'ec-user > div' element (unique child)
     if (node.matches(
@@ -141,17 +120,9 @@
     // Inject avatar links to threads in the thread list. injectIfEnabled is
     // responsible of determining whether it should run or not depending on its
     // current setting.
-    //
-    // Also, inject extra info in the thread list.
     if (('tagName' in node) && (node.tagName == 'LI') &&
         node.querySelector('ec-thread-summary') !== null) {
       avatars.injectIfEnabled(node);
-      window.TWPTExtraInfo.injectAtThreadListIfEnabled(node);
-    }
-
-    // Inject extra info in the toolbelt of an expanded thread list item.
-    if (node.matches('ec-thread-summary .main .toolbelt')) {
-      window.TWPTExtraInfo.injectAtExpandedThreadListIfEnabled(node);
     }
 
     if (node.tagName == 'IFRAME') {
@@ -170,28 +141,6 @@
       window.TWPTWorkflowsImport.addButtonIfEnabled(node);
     }
 
-    // Show additional details in the thread view.
-    if (node.matches(
-            'sc-tailwind-thread-question-question-card sc-tailwind-thread-question-state-chips')) {
-      window.TWPTExtraInfo.injectAtQuestionIfEnabled(node);
-    }
-    if (node.matches(
-            'sc-tailwind-thread-message-message-list sc-tailwind-thread-message-message-card')) {
-      window.TWPTExtraInfo.injectAtReplyIfEnabled(node);
-    }
-
-    if (node.matches(
-            'sc-tailwind-thread-message-message-list sc-tailwind-thread-message-comment-card')) {
-      window.TWPTExtraInfo.injectAtCommentIfEnabled(node);
-    }
-
-    // Inject per-forum stats section in the user profile
-    if (node.matches(
-            'ec-unified-user .scTailwindUser_profileUserprofilesection ' +
-            'sc-tailwind-shared-activity-chart')) {
-      window.TWPTExtraInfo.injectPerForumStatsIfEnabled(node);
-    }
-
     // Inject old thread page design warning if applicable
     if (node.matches(
             'ec-thread > .page > .material-content > div[role="list"]')) {
@@ -259,7 +208,7 @@
   flattenThreads = new FlattenThreads();
   reportDialogColorThemeFix = new ReportDialogColorThemeFix(options);
 
-  // extraInfo, threadPageDesignWarning and workflowsImport are
+  // threadPageDesignWarning and workflowsImport are
   // initialized in start.js
 
   // Before starting the mutation Observer, check whether we missed any
@@ -313,9 +262,6 @@
   injectStylesheet(chrome.runtime.getURL('css/batchlock_inject.css'));
   // Thread list avatars
   injectStylesheet(chrome.runtime.getURL('css/thread_list_avatars.css'));
-  // Extra info
-  injectStylesheet(chrome.runtime.getURL('css/extrainfo.css'));
-  injectStylesheet(chrome.runtime.getURL('css/extrainfo_perforumstats.css'));
   // Workflows, Thread toolbar
   injectScript(chrome.runtime.getURL('litComponentsInject.bundle.js'));
   // Thread toolbar
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index 33d2039..a0640ba 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -1,7 +1,6 @@
-import {injectScript, injectStylesheet} from '../../common/contentScriptsUtils.js';
+import {injectStylesheet} from '../../common/contentScriptsUtils.js';
 import {getOptions} from '../../common/optionsUtils.js';
 
-import ExtraInfo from './extraInfo/index.js';
 import FlattenThreadsReplyActionHandler from './flattenThreads/replyActionHandler.js';
 import ThreadPageDesignWarning from './threadPageDesignWarning.js';
 import WorkflowsImport from './workflows/import.js';
@@ -9,8 +8,6 @@
 const SMEI_NESTED_REPLIES = 15;
 const SMEI_RCE_THREAD_INTEROP = 22;
 
-injectScript(chrome.runtime.getURL('extraInfoInject.bundle.js'));
-
 getOptions(null).then(options => {
   /* IMPORTANT NOTE: Remember to change this when changing the "ifs" below!! */
   if (options.loaddrafts || options.interopthreadpage ||
@@ -47,7 +44,6 @@
 
   // Initialized here instead of in main.js so the first event is received if it
   // happens when the page loads.
-  window.TWPTExtraInfo = new ExtraInfo();
   window.TWPTThreadPageDesignWarning = new ThreadPageDesignWarning();
   window.TWPTWorkflowsImport = new WorkflowsImport();
 
@@ -71,6 +67,7 @@
     injectStylesheet(chrome.runtime.getURL('css/ui_spacing/console.css'));
   }
 
-  const flattenThreadsReplyActionHandler = new FlattenThreadsReplyActionHandler(options);
+  const flattenThreadsReplyActionHandler =
+      new FlattenThreadsReplyActionHandler(options);
   flattenThreadsReplyActionHandler.handleIfApplicable();
 });
diff --git a/src/features/Features.ts b/src/features/Features.ts
index 6eab170..41e6b5d 100644
--- a/src/features/Features.ts
+++ b/src/features/Features.ts
@@ -2,12 +2,14 @@
 import AutoRefreshFeature from './autoRefresh/autoRefresh.feature';
 import InfiniteScrollFeature from './infiniteScroll/infiniteScroll.feature';
 import ScriptFilterListProvider from '../common/architecture/scripts/ScriptFilterListProvider';
+import ExtraInfoFeature from './extraInfo/extraInfo.feature';
 
 export type ConcreteFeatureClass = { new (): Feature };
 
 export default class Features extends ScriptFilterListProvider {
   private features: ConcreteFeatureClass[] = [
     AutoRefreshFeature,
+    ExtraInfoFeature,
     InfiniteScrollFeature,
   ];
   private initializedFeatures: Feature[];
diff --git a/src/contentScripts/communityConsole/extraInfo/consts.js b/src/features/extraInfo/core/consts.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/consts.js
rename to src/features/extraInfo/core/consts.js
diff --git a/src/contentScripts/communityConsole/extraInfo/index.js b/src/features/extraInfo/core/index.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/index.js
rename to src/features/extraInfo/core/index.js
diff --git a/src/contentScripts/communityConsole/extraInfo/infoHandlers/base.js b/src/features/extraInfo/core/infoHandlers/base.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/infoHandlers/base.js
rename to src/features/extraInfo/core/infoHandlers/base.js
diff --git a/src/contentScripts/communityConsole/extraInfo/infoHandlers/basedOnResponseEvent.js b/src/features/extraInfo/core/infoHandlers/basedOnResponseEvent.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/infoHandlers/basedOnResponseEvent.js
rename to src/features/extraInfo/core/infoHandlers/basedOnResponseEvent.js
diff --git a/src/contentScripts/communityConsole/extraInfo/infoHandlers/profile.js b/src/features/extraInfo/core/infoHandlers/profile.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/infoHandlers/profile.js
rename to src/features/extraInfo/core/infoHandlers/profile.js
diff --git a/src/contentScripts/communityConsole/extraInfo/infoHandlers/thread.js b/src/features/extraInfo/core/infoHandlers/thread.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/infoHandlers/thread.js
rename to src/features/extraInfo/core/infoHandlers/thread.js
diff --git a/src/contentScripts/communityConsole/extraInfo/infoHandlers/threadList.js b/src/features/extraInfo/core/infoHandlers/threadList.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/infoHandlers/threadList.js
rename to src/features/extraInfo/core/infoHandlers/threadList.js
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/base.js b/src/features/extraInfo/core/injections/base.js
similarity index 96%
rename from src/contentScripts/communityConsole/extraInfo/injections/base.js
rename to src/features/extraInfo/core/injections/base.js
index 78ded47..66c024e 100644
--- a/src/contentScripts/communityConsole/extraInfo/injections/base.js
+++ b/src/features/extraInfo/core/injections/base.js
@@ -1,7 +1,7 @@
 import {MDCTooltip} from '@material/tooltip';
 
 import {shouldImplement} from '../../../../common/commonUtils.js';
-import {createExtBadge} from '../../utils/common.js';
+import {createExtBadge} from '../../../../contentScripts/communityConsole/utils/common.js';
 
 export default class BaseExtraInfoInjection {
   constructor(infoHandler, optionsWatcher) {
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/baseThreadMessage.js b/src/features/extraInfo/core/injections/baseThreadMessage.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/injections/baseThreadMessage.js
rename to src/features/extraInfo/core/injections/baseThreadMessage.js
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/expandedThreadList.js b/src/features/extraInfo/core/injections/expandedThreadList.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/injections/expandedThreadList.js
rename to src/features/extraInfo/core/injections/expandedThreadList.js
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/profileAbuse.js b/src/features/extraInfo/core/injections/profileAbuse.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/injections/profileAbuse.js
rename to src/features/extraInfo/core/injections/profileAbuse.js
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/profilePerForumStats.js b/src/features/extraInfo/core/injections/profilePerForumStats.js
similarity index 73%
rename from src/contentScripts/communityConsole/extraInfo/injections/profilePerForumStats.js
rename to src/features/extraInfo/core/injections/profilePerForumStats.js
index 57278ee..83de9db 100644
--- a/src/contentScripts/communityConsole/extraInfo/injections/profilePerForumStats.js
+++ b/src/features/extraInfo/core/injections/profilePerForumStats.js
@@ -1,5 +1,5 @@
-import {getDisplayLanguage} from '../../utils/common.js';
-import PerForumStatsSection from '../../utils/PerForumStatsSection.js';
+import {getDisplayLanguage} from '../../../../contentScripts/communityConsole/utils/common.js';
+import PerForumStatsSection from '../../../../contentScripts/communityConsole/utils/PerForumStatsSection.js';
 
 import BaseExtraInfoInjection from './base.js';
 
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/threadComment.js b/src/features/extraInfo/core/injections/threadComment.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/injections/threadComment.js
rename to src/features/extraInfo/core/injections/threadComment.js
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/threadList.js b/src/features/extraInfo/core/injections/threadList.js
similarity index 95%
rename from src/contentScripts/communityConsole/extraInfo/injections/threadList.js
rename to src/features/extraInfo/core/injections/threadList.js
index 6a78ea3..e35fbe0 100644
--- a/src/contentScripts/communityConsole/extraInfo/injections/threadList.js
+++ b/src/features/extraInfo/core/injections/threadList.js
@@ -1,7 +1,7 @@
 import {MDCTooltip} from '@material/tooltip';
 
 import {parseUrl} from '../../../../common/commonUtils.js';
-import {createExtBadge} from '../../utils/common.js';
+import {createExtBadge} from '../../../../contentScripts/communityConsole/utils/common.js';
 import {kItemMetadataState, kItemMetadataStateI18n} from '../consts.js';
 import ThreadExtraInfoService from '../services/thread.js';
 
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/threadQuestion.js b/src/features/extraInfo/core/injections/threadQuestion.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/injections/threadQuestion.js
rename to src/features/extraInfo/core/injections/threadQuestion.js
diff --git a/src/contentScripts/communityConsole/extraInfo/injections/threadReply.js b/src/features/extraInfo/core/injections/threadReply.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/injections/threadReply.js
rename to src/features/extraInfo/core/injections/threadReply.js
diff --git a/src/contentScripts/communityConsole/extraInfo/services/message.js b/src/features/extraInfo/core/services/message.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/services/message.js
rename to src/features/extraInfo/core/services/message.js
diff --git a/src/contentScripts/communityConsole/extraInfo/services/states.js b/src/features/extraInfo/core/services/states.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/services/states.js
rename to src/features/extraInfo/core/services/states.js
diff --git a/src/contentScripts/communityConsole/extraInfo/services/thread.js b/src/features/extraInfo/core/services/thread.js
similarity index 100%
rename from src/contentScripts/communityConsole/extraInfo/services/thread.js
rename to src/features/extraInfo/core/services/thread.js
diff --git a/src/features/extraInfo/extraInfo.feature.ts b/src/features/extraInfo/extraInfo.feature.ts
new file mode 100644
index 0000000..f7f9f66
--- /dev/null
+++ b/src/features/extraInfo/extraInfo.feature.ts
@@ -0,0 +1,18 @@
+import Feature from '../../common/architecture/features/Feature';
+import { ConcreteScript } from '../../common/architecture/scripts/Script';
+import CCExtraInfoDependencySetUpScript from './scripts/ccExtraInfoDependencySetUp.script';
+import CCExtraInfoInjectScript from './scripts/ccExtraInfoInject.script';
+import CCExtraInfoMainScript from './scripts/ccExtraInfoMain.script';
+import CCExtraInfoStylesScript from './scripts/ccExtraInfoStyles.script';
+
+export default class ExtraInfoFeature extends Feature {
+  public readonly scripts: ConcreteScript[] = [
+    CCExtraInfoDependencySetUpScript,
+    CCExtraInfoInjectScript,
+    CCExtraInfoMainScript,
+    CCExtraInfoStylesScript,
+  ];
+
+  readonly codename = 'extraInfo';
+  readonly relatedOptions: string[] = [];
+}
diff --git a/src/features/extraInfo/nodeWatcherHandlers/profile/ccExtraInfoProfileAbuseChips.handler.ts b/src/features/extraInfo/nodeWatcherHandlers/profile/ccExtraInfoProfileAbuseChips.handler.ts
new file mode 100644
index 0000000..7cc895a
--- /dev/null
+++ b/src/features/extraInfo/nodeWatcherHandlers/profile/ccExtraInfoProfileAbuseChips.handler.ts
@@ -0,0 +1,11 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../../common/nodeWatcher/NodeWatcherHandler';
+import { CCExtraInfoMainOptions } from '../../scripts/ccExtraInfoMain.script';
+
+export default class CCExtraInfoProfileAbuseChipsHandler extends CssSelectorNodeWatcherScriptHandler<CCExtraInfoMainOptions> {
+  cssSelector = 'ec-unified-user .scTailwindUser_profileUsercardmain';
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.options.extraInfo.injectAbuseChipsAtProfileIfEnabled(node);
+  }
+}
diff --git a/src/features/extraInfo/nodeWatcherHandlers/profile/ccExtraInfoProfilePerForumStats.handler.ts b/src/features/extraInfo/nodeWatcherHandlers/profile/ccExtraInfoProfilePerForumStats.handler.ts
new file mode 100644
index 0000000..d08db21
--- /dev/null
+++ b/src/features/extraInfo/nodeWatcherHandlers/profile/ccExtraInfoProfilePerForumStats.handler.ts
@@ -0,0 +1,12 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../../common/nodeWatcher/NodeWatcherHandler';
+import { CCExtraInfoMainOptions } from '../../scripts/ccExtraInfoMain.script';
+
+export default class CCExtraInfoProfilePerForumStatsHandler extends CssSelectorNodeWatcherScriptHandler<CCExtraInfoMainOptions> {
+  cssSelector =
+    'ec-unified-user .scTailwindUser_profileUserprofilesection sc-tailwind-shared-activity-chart';
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.options.extraInfo.injectPerForumStatsIfEnabled(node);
+  }
+}
diff --git a/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadComment.handler.ts b/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadComment.handler.ts
new file mode 100644
index 0000000..802dc51
--- /dev/null
+++ b/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadComment.handler.ts
@@ -0,0 +1,14 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../../common/nodeWatcher/NodeWatcherHandler';
+import { CCExtraInfoMainOptions } from '../../scripts/ccExtraInfoMain.script';
+
+/**
+ * Inject extra info to threads in the thread list.
+ */
+export default class CCExtraInfoThreadCommentHandler extends CssSelectorNodeWatcherScriptHandler<CCExtraInfoMainOptions> {
+  cssSelector = 'sc-tailwind-thread-message-message-list sc-tailwind-thread-message-comment-card';
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.options.extraInfo.injectAtCommentIfEnabled(node);
+  }
+}
diff --git a/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadQuestion.handler.ts b/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadQuestion.handler.ts
new file mode 100644
index 0000000..0d16772
--- /dev/null
+++ b/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadQuestion.handler.ts
@@ -0,0 +1,14 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../../common/nodeWatcher/NodeWatcherHandler';
+import { CCExtraInfoMainOptions } from '../../scripts/ccExtraInfoMain.script';
+
+/**
+ * Inject extra info to threads in the thread list.
+ */
+export default class CCExtraInfoThreadQuestionHandler extends CssSelectorNodeWatcherScriptHandler<CCExtraInfoMainOptions> {
+  cssSelector = 'sc-tailwind-thread-question-question-card sc-tailwind-thread-question-state-chips';
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.options.extraInfo.injectAtQuestionIfEnabled(node);
+  }
+}
diff --git a/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadReply.handler.ts b/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadReply.handler.ts
new file mode 100644
index 0000000..543ddb0
--- /dev/null
+++ b/src/features/extraInfo/nodeWatcherHandlers/thread/ccExtraInfoThreadReply.handler.ts
@@ -0,0 +1,14 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../../common/nodeWatcher/NodeWatcherHandler';
+import { CCExtraInfoMainOptions } from '../../scripts/ccExtraInfoMain.script';
+
+/**
+ * Inject extra info to threads in the thread list.
+ */
+export default class CCExtraInfoThreadReplyHandler extends CssSelectorNodeWatcherScriptHandler<CCExtraInfoMainOptions> {
+  cssSelector = 'sc-tailwind-thread-message-message-list sc-tailwind-thread-message-message-card';
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.options.extraInfo.injectAtReplyIfEnabled(node);
+  }
+}
diff --git a/src/features/extraInfo/nodeWatcherHandlers/threadList/ccExtraInfoThreadList.handler.ts b/src/features/extraInfo/nodeWatcherHandlers/threadList/ccExtraInfoThreadList.handler.ts
new file mode 100644
index 0000000..5d26037
--- /dev/null
+++ b/src/features/extraInfo/nodeWatcherHandlers/threadList/ccExtraInfoThreadList.handler.ts
@@ -0,0 +1,14 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../../common/nodeWatcher/NodeWatcherHandler';
+import { CCExtraInfoMainOptions } from '../../scripts/ccExtraInfoMain.script';
+
+/**
+ * Inject extra info to threads in the thread list.
+ */
+export default class CCExtraInfoThreadListHandler extends CssSelectorNodeWatcherScriptHandler<CCExtraInfoMainOptions> {
+  cssSelector = 'li:has(ec-thread-summary)';
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.options.extraInfo.injectAtThreadListIfEnabled(node);
+  }
+}
diff --git a/src/features/extraInfo/nodeWatcherHandlers/threadList/ccExtraInfoThreadListToolbelt.handler.ts b/src/features/extraInfo/nodeWatcherHandlers/threadList/ccExtraInfoThreadListToolbelt.handler.ts
new file mode 100644
index 0000000..b6ef4dc
--- /dev/null
+++ b/src/features/extraInfo/nodeWatcherHandlers/threadList/ccExtraInfoThreadListToolbelt.handler.ts
@@ -0,0 +1,14 @@
+import CssSelectorNodeWatcherScriptHandler from '../../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { NodeMutation } from '../../../../common/nodeWatcher/NodeWatcherHandler';
+import { CCExtraInfoMainOptions } from '../../scripts/ccExtraInfoMain.script';
+
+/**
+ * Inject extra info in the toolbelt of an expanded thread list item.
+ */
+export default class CCExtraInfoThreadListToolbeltHandler extends CssSelectorNodeWatcherScriptHandler<CCExtraInfoMainOptions> {
+  cssSelector = 'ec-thread-summary .main .toolbelt';
+
+  onMutatedNode({ node }: NodeMutation) {
+    this.options.extraInfo.injectAtExpandedThreadListIfEnabled(node);
+  }
+}
diff --git a/src/features/extraInfo/scripts/ccExtraInfoDependencySetUp.script.ts b/src/features/extraInfo/scripts/ccExtraInfoDependencySetUp.script.ts
new file mode 100644
index 0000000..cd595c5
--- /dev/null
+++ b/src/features/extraInfo/scripts/ccExtraInfoDependencySetUp.script.ts
@@ -0,0 +1,15 @@
+import { Dependency, ExtraInfoDependency } 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 CCExtraInfoDependencySetUpScript extends SetUpDependenciesScript {
+  public priority = 101;
+  public page = ScriptPage.CommunityConsole;
+  public environment = ScriptEnvironment.ContentScript;
+  public runPhase = ScriptRunPhase.Start;
+  public dependencies: Dependency[] = [ExtraInfoDependency];
+}
diff --git a/src/features/extraInfo/scripts/ccExtraInfoInject.script.ts b/src/features/extraInfo/scripts/ccExtraInfoInject.script.ts
new file mode 100644
index 0000000..2bd6364
--- /dev/null
+++ b/src/features/extraInfo/scripts/ccExtraInfoInject.script.ts
@@ -0,0 +1,14 @@
+import Script, { ScriptEnvironment, ScriptPage, ScriptRunPhase } from "../../../common/architecture/scripts/Script";
+import { injectScript } from "../../../common/contentScriptsUtils";
+
+export default class CCExtraInfoInjectScript extends Script {
+  priority = 11;
+
+  page = ScriptPage.CommunityConsole;
+  environment = ScriptEnvironment.ContentScript;
+  runPhase = ScriptRunPhase.Start;
+
+  execute() {
+    injectScript(chrome.runtime.getURL('extraInfoInject.bundle.js'));
+  }
+}
diff --git a/src/features/extraInfo/scripts/ccExtraInfoMain.script.ts b/src/features/extraInfo/scripts/ccExtraInfoMain.script.ts
new file mode 100644
index 0000000..80fbfbf
--- /dev/null
+++ b/src/features/extraInfo/scripts/ccExtraInfoMain.script.ts
@@ -0,0 +1,43 @@
+import DependenciesProviderSingleton, {
+  ExtraInfoDependency,
+} from '../../../common/architecture/dependenciesProvider/DependenciesProvider';
+import {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../../../common/architecture/scripts/Script';
+import NodeWatcherScript from '../../../common/architecture/scripts/nodeWatcher/NodeWatcherScript';
+import ExtraInfo from '../core';
+import CCExtraInfoProfileAbuseChipsHandler from '../nodeWatcherHandlers/profile/ccExtraInfoProfileAbuseChips.handler';
+import CCExtraInfoProfilePerForumStatsHandler from '../nodeWatcherHandlers/profile/ccExtraInfoProfilePerForumStats.handler';
+import CCExtraInfoThreadCommentHandler from '../nodeWatcherHandlers/thread/ccExtraInfoThreadComment.handler';
+import CCExtraInfoThreadListHandler from '../nodeWatcherHandlers/threadList/ccExtraInfoThreadList.handler';
+import CCExtraInfoThreadListToolbeltHandler from '../nodeWatcherHandlers/threadList/ccExtraInfoThreadListToolbelt.handler';
+import CCExtraInfoThreadQuestionHandler from '../nodeWatcherHandlers/thread/ccExtraInfoThreadQuestion.handler';
+import CCExtraInfoThreadReplyHandler from '../nodeWatcherHandlers/thread/ccExtraInfoThreadReply.handler';
+
+export interface CCExtraInfoMainOptions {
+  extraInfo: ExtraInfo;
+}
+
+export default class CCExtraInfoMainScript extends NodeWatcherScript<CCExtraInfoMainOptions> {
+  page = ScriptPage.CommunityConsole;
+  environment = ScriptEnvironment.ContentScript;
+  runPhase = ScriptRunPhase.Main;
+  handlers = new Map([
+    ['ccExtraInfoProfile', CCExtraInfoProfileAbuseChipsHandler],
+    ['ccExtraInfoProfilePerForumStats', CCExtraInfoProfilePerForumStatsHandler],
+    ['ccExtraInfoThreadComment', CCExtraInfoThreadCommentHandler],
+    ['ccExtraInfoThreadList', CCExtraInfoThreadListHandler],
+    ['ccExtraInfoThreadListToolbelt', CCExtraInfoThreadListToolbeltHandler],
+    ['ccExtraInfoThreadQuestion', CCExtraInfoThreadQuestionHandler],
+    ['ccExtraInfoThreadReply', CCExtraInfoThreadReplyHandler],
+  ]);
+
+  protected optionsFactory(): CCExtraInfoMainOptions {
+    const dependenciesProvider = DependenciesProviderSingleton.getInstance();
+    return {
+      extraInfo: dependenciesProvider.getDependency(ExtraInfoDependency),
+    };
+  }
+}
diff --git a/src/features/extraInfo/scripts/ccExtraInfoStyles.script.ts b/src/features/extraInfo/scripts/ccExtraInfoStyles.script.ts
new file mode 100644
index 0000000..4d806f6
--- /dev/null
+++ b/src/features/extraInfo/scripts/ccExtraInfoStyles.script.ts
@@ -0,0 +1,17 @@
+import Script, {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../../../common/architecture/scripts/Script';
+import { injectStylesheet } from '../../../common/contentScriptsUtils';
+
+export default class CCExtraInfoStylesScript extends Script {
+  page = ScriptPage.CommunityConsole;
+  environment = ScriptEnvironment.ContentScript;
+  runPhase = ScriptRunPhase.Main;
+
+  execute() {
+    injectStylesheet(chrome.runtime.getURL('css/extrainfo.css'));
+    injectStylesheet(chrome.runtime.getURL('css/extrainfo_perforumstats.css'));
+  }
+}
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index f7b2923..2f3f4cf 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -178,16 +178,16 @@
   "minimum_chrome_version": "96",
 #endif
 #if defined(CHROMIUM_MV3)
-  "minimum_chrome_version": "100",
+  "minimum_chrome_version": "105",
 #endif
 #if defined(GECKO)
   "browser_specific_settings": {
     "gecko": {
       "id": "twpowertools@avm99963.com",
-      "strict_min_version": "102.0"
+      "strict_min_version": "121.0"
     },
     "gecko_android": {
-      "strict_min_version": "102.0"
+      "strict_min_version": "121.0"
     }
   },
 #endif