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

Bug: twpowertools:176
Change-Id: I7a24fb504ce53697112f11128d2d5249a5a7c7e7
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js b/src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js
deleted file mode 100644
index 7f93e9a..0000000
--- a/src/contentScripts/communityConsole/flattenThreads/components/QuoteAuthor.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import '@material/web/icon/icon.js';
-import '@material/web/iconbutton/icon-button.js';
-
-import {css, html, LitElement} from 'lit';
-
-import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
-
-export default class TwptFlattenThreadQuoteAuthor extends LitElement {
-  static properties = {
-    prevMessage: {type: Object},
-  };
-
-  static styles = [
-    SHARED_MD3_STYLES,
-    css`
-    :host {
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-      max-width: max(25%, 150px);
-      color: var(--TWPT-interop-secondary-text, #444746);
-    }
-
-    :host > *:not(:last-child) {
-      margin-right: 4px;
-    }
-
-    .reply-icon {
-      font-size: 20px;
-    }
-
-    .name {
-      overflow: hidden;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-      font-size: 15px;
-    }
-    `,
-  ];
-
-  constructor() {
-    super();
-    this.prevMessage = {};
-  }
-
-  render() {
-    return html`
-      <md-icon class="reply-icon">reply</md-icon>
-      <span class="name">${this.prevMessage?.author?.[1]?.[1]}</span>
-      <md-icon-button
-          @click=${this.focusParent}>
-        <md-icon>arrow_upward</md-icon>
-      </md-icon-button>
-    `;
-  }
-
-  focusParent() {
-    const parentNode = document.querySelector(
-        '[data-twpt-message-id="' + this.prevMessage?.id + '"]');
-    parentNode.focus({preventScroll: true});
-    parentNode.scrollIntoView({behavior: 'smooth', block: 'start'});
-  }
-}
-window.customElements.define(
-    'twpt-flatten-thread-quote-author', TwptFlattenThreadQuoteAuthor);
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js b/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js
deleted file mode 100644
index 8adf676..0000000
--- a/src/contentScripts/communityConsole/flattenThreads/components/ReplyButton.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import '@material/web/button/outlined-button.js';
-
-import {msg, str} from '@lit/localize';
-import {css, html} from 'lit';
-
-import {I18nLitElement} from '../../../../common/litI18nUtils.js';
-import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
-import {openReplyEditor} from '../../utils/common.js';
-import {getExtraInfoNodes} from '../flattenThreads.js';
-
-export default class TwptFlattenThreadReplyButton extends I18nLitElement {
-  static properties = {
-    extraInfo: {type: Object},
-  };
-
-  static styles = [
-    SHARED_MD3_STYLES,
-    css`
-    md-outlined-button {
-      --md-outlined-button-container-shape: 0.25rem;
-      --md-outlined-button-container-height: 38px;
-
-      /**
-       * We change the color because otherwise it would have a very similar
-       * color to the "Recommend" button.
-       */
-      --md-outlined-button-label-text-color: var(--reply-button-color);
-      --md-outlined-button-focus-label-text-color: var(--reply-button-color);
-      --md-outlined-button-hover-label-text-color: var(--reply-button-color);
-      --md-outlined-button-hover-state-layer-color: var(--reply-button-color);
-      --md-outlined-button-label-text-color: var(--reply-button-color);
-      --md-outlined-button-pressed-label-text-color: var(--reply-button-color);
-      --md-outlined-button-pressed-state-layer-color: var(--reply-button-color);
-      --md-outlined-button-focus-icon-color: var(--reply-button-color);
-      --md-outlined-button-hover-icon-color: var(--reply-button-color);
-      --md-outlined-button-icon-color: var(--reply-button-color);
-      --md-outlined-button-pressed-icon-color: var(--reply-button-color);
-    }
-    `,
-  ];
-
-  constructor() {
-    super();
-    this.extraInfo = {};
-    this.addEventListener('twpt-click', this.openReplyEditor);
-  }
-
-  render() {
-    return html`
-      <md-outlined-button
-          @click=${this.openReplyEditor}>
-        ${msg('Reply', {
-      desc: 'Button which is used to open the reply box.',
-    })}
-      </md-outlined-button>
-    `;
-  }
-
-  #defaultReply(messagePayload) {
-    const quoteHeader = document.createElement('div');
-    const italics = document.createElement('i');
-    const authorName = this.extraInfo?.authorName;
-    italics.textContent = msg(str`${authorName} said:`);
-    quoteHeader.append(italics);
-
-    const quote = document.createElement('blockquote');
-    quote.innerHTML = messagePayload;
-    getExtraInfoNodes(quote)?.forEach?.(node => {
-      node.parentNode.removeChild(node);
-    });
-
-    const br1 = document.createElement('br');
-    const br2 = document.createElement('br');
-
-    return [quoteHeader, quote, br1, br2];
-  }
-
-  openReplyEditor() {
-    const messageId = this.extraInfo?.id;
-    const messagePayload = document.querySelector(
-        '[data-twpt-message-id="' + messageId +
-        '"] .scTailwindThreadPostcontentroot html-blob');
-    if (!messagePayload) {
-      console.error('[flattenThreads] Payload not found.');
-      return;
-    }
-
-    const parentId = this.extraInfo?.parentId ?? this.extraInfo?.id;
-    openReplyEditor(parentId).then(editor => {
-      const payload =
-          editor?.querySelector('.scTailwindSharedRichtexteditoreditor');
-
-      payload.prepend(...this.#defaultReply(messagePayload.innerHTML));
-      payload.scrollTop = payload.scrollHeight;
-    });
-  }
-}
-window.customElements.define(
-    'twpt-flatten-thread-reply-button', TwptFlattenThreadReplyButton);
diff --git a/src/contentScripts/communityConsole/flattenThreads/components/index.js b/src/contentScripts/communityConsole/flattenThreads/components/index.js
deleted file mode 100644
index f1da3b0..0000000
--- a/src/contentScripts/communityConsole/flattenThreads/components/index.js
+++ /dev/null
@@ -1,153 +0,0 @@
-import '@material/web/button/filled-tonal-button.js';
-import '@material/web/icon/icon.js';
-import './QuoteAuthor.js';
-
-// Other components imported so they are also injected:
-import './ReplyButton.js';
-
-import {msg} from '@lit/localize';
-import * as DOMPurify from 'dompurify';
-import {css, html} from 'lit';
-import {classMap} from 'lit/directives/class-map.js';
-import {unsafeHTML} from 'lit/directives/unsafe-html.js';
-
-import {I18nLitElement} from '../../../../common/litI18nUtils.js';
-import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
-
-export default class TwptFlattenThreadQuote extends I18nLitElement {
-  static properties = {
-    prevMessage: {type: Object},
-    _expanded: {type: Boolean},
-  };
-
-  static styles = [
-    SHARED_MD3_STYLES,
-    css`
-    :host {
-      display: block;
-    }
-
-    .quote-container {
-      position: relative;
-      background-color: var(--TWPT-secondary-background, rgba(242, 242, 242, 0.502));
-      margin-bottom: 16px;
-      padding: 0 12px 12px 12px;
-      border-radius: 12px;
-    }
-
-    .payload-container {
-      padding-top: 12px;
-    }
-
-    .quote-container:not(.quote-container--expanded) .payload-container {
-      max-height: 2.8rem;
-      overflow: hidden;
-      mask-image: linear-gradient(rgb(0, 0, 0) 76%, transparent);
-      -webkit-mask-image: linear-gradient(rgb(0, 0, 0) 76%, transparent);
-    }
-
-    .payload-container twpt-flatten-thread-quote-author {
-      float: right;
-      margin-left: 12px;
-      margin-top: -8px;
-    }
-
-    .payload {
-      display: inline;
-    }
-
-    .payload img {
-      max-width: 100%;
-      max-height: calc(100vh - 2*64px);
-    }
-
-    .payload blockquote {
-      border-left: 1px solid #757575;
-      margin: 0 0 0 4px;
-      padding: 0 0 0 4px;
-    }
-
-    .buttons-row {
-      position: absolute;
-      width: calc(100% - 24px);
-      display: flex;
-      flex-direction: row;
-      justify-content: center;
-      bottom: -20px;
-      transition: opacity .25s;
-    }
-
-    @media (hover: hover) {
-      @supports selector(:has(div)) {
-        .quote-container:not(:hover) .buttons-row:not(:focus-within) {
-          opacity: 0;
-        }
-      }
-    }
-
-    .buttons-row md-filled-tonal-button {
-      --md-filled-tonal-button-container-color: var(--TWPT-dark-flatten-replies-more-bg, rgba(222, 222, 222, 0.9));
-      --md-filled-tonal-button-label-text-color: var(--TWPT-md-sys-color-on-surface);
-      --md-filled-tonal-button-hover-label-text-color: var(--TWPT-md-sys-color-on-surface);
-      --md-filled-tonal-button-focus-label-text-color: var(--TWPT-md-sys-color-on-surface);
-      --md-filled-tonal-button-pressed-label-text-color: var(--TWPT-md-sys-color-on-surface);
-      --md-filled-tonal-button-icon-color: var(--TWPT-md-sys-color-on-surface);
-      --md-filled-tonal-button-hover-icon-color: var(--TWPT-md-sys-color-on-surface);
-      --md-filled-tonal-button-focus-icon-color: var(--TWPT-md-sys-color-on-surface);
-      --md-filled-tonal-button-pressed-icon-color: var(--TWPT-md-sys-color-on-surface);
-    }
-    `,
-  ];
-
-  constructor() {
-    super();
-    this.prevMessage = {};
-    this._expanded = false;
-  }
-
-  getTrustedPayload() {
-    return DOMPurify.sanitize(this.prevMessage?.payload ?? '');
-  }
-
-  render() {
-    const containerClasses = classMap({
-      'quote-container': true,
-      'quote-container--expanded': this._expanded,
-    });
-    const lessMsg = msg('Less', {
-      desc:
-          'Button to collapse the quote message (used in the flatten threads feature).',
-    });
-    const moreMsg = msg('More', {
-      desc:
-          'Button to expand the quote message (used in the flatten threads feature).',
-    });
-    return html`
-      <div class=${containerClasses}>
-        <div class="payload-container">
-          <twpt-flatten-thread-quote-author
-              .prevMessage=${this.prevMessage}>
-          </twpt-flatten-thread-quote-author>
-          <div class="payload">
-            ${unsafeHTML(this.getTrustedPayload())}
-          </div>
-        </div>
-        <div class="buttons-row">
-          <md-filled-tonal-button
-              @click=${this.toggleExpanded}>
-            <md-icon slot="icon">
-              ${this._expanded ? 'expand_less' : 'expand_more'}
-            </md-icon>
-            ${this._expanded ? lessMsg : moreMsg}
-          </md-filled-tonal-button>
-        </div>
-      </div>
-    `;
-  }
-
-  toggleExpanded() {
-    this._expanded = !this._expanded;
-  }
-}
-window.customElements.define(
-    'twpt-flatten-thread-quote', TwptFlattenThreadQuote);
diff --git a/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js b/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js
deleted file mode 100644
index 557a533..0000000
--- a/src/contentScripts/communityConsole/flattenThreads/flattenThreads.js
+++ /dev/null
@@ -1,86 +0,0 @@
-export const kAdditionalInfoClass = 'ck-indent-9996300035194';
-
-export const kReplyPayloadSelector =
-    '.scTailwindThreadMessageMessagecardcontent:not(.scTailwindThreadMessageMessagecardpromoted) .scTailwindThreadPostcontentroot html-blob';
-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);
-}
-
-export default class FlattenThreads {
-  construct() {}
-
-  getExtraInfo(node) {
-    const extraInfoNode = node.querySelector(kAdditionalInfoSelector);
-    if (!extraInfoNode) return null;
-    return JSON.parse(extraInfoNode.textContent);
-  }
-
-  injectId(node, extraInfo) {
-    const root = node.closest('.scTailwindThreadMessageMessagecardcontent');
-    if (!root) return false;
-    root.setAttribute('data-twpt-message-id', extraInfo.id);
-    return true;
-  }
-
-  injectQuote(node, extraInfo) {
-    const content = node.closest('.scTailwindThreadPostcontentroot');
-    const quote = document.createElement('twpt-flatten-thread-quote');
-    quote.setAttribute('prevMessage', JSON.stringify(extraInfo.prevMessage));
-    content.prepend(quote);
-  }
-
-  injectReplyBtn(node, extraInfo) {
-    const btn = document.createElement('twpt-flatten-thread-reply-button');
-    btn.setAttribute('extraInfo', JSON.stringify(extraInfo));
-    node.prepend(btn);
-  }
-
-  injectQuoteIfApplicable(node) {
-    // If we injected the additional information, it means the flatten threads
-    // feature is enabled and in actual use, so we should inject the quote.
-    const extraInfo = this.getExtraInfo(node);
-    if (!extraInfo) return;
-
-    this.injectId(node, extraInfo);
-    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
-    // button.
-    const root =
-        node.closest('.scTailwindThreadMessageMessagecardcontent')
-            .querySelector('.scTailwindThreadMessageMessagecardbody html-blob');
-    const extraInfo = this.getExtraInfo(root);
-    if (!extraInfo || !extraInfo.canComment) return;
-
-    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/contentScripts/communityConsole/flattenThreads/replyActionHandler.js
deleted file mode 100644
index 182f962..0000000
--- a/src/contentScripts/communityConsole/flattenThreads/replyActionHandler.js
+++ /dev/null
@@ -1,74 +0,0 @@
-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;
-  }
-
-  async handleIfApplicable() {
-    if (await this.isFeatureEnabled()) this.handle();
-  }
-
-  async handle() {
-    const hash = window.location.hash;
-    if (hash === '#action=reply') await this.openReplyEditor();
-  }
-
-  async openReplyEditor() {
-    // We erase the hash so the Community Console doesn't open the reply
-    // editor, since we're going to do that instead.
-    window.location.hash = '';
-
-    const messageId = this.getCurrentMessageId();
-    if (messageId === null) {
-      console.error(
-          '[FlattenThreadsReplyActionHandler] Could not parse current message id.');
-      return;
-    }
-
-    const replyButton = await waitFor(async () => {
-      const replyButton = document.querySelector(
-          '[data-twpt-message-id="' + messageId +
-          '"] twpt-flatten-thread-reply-button');
-      if (replyButton === null) throw new Error('Reply button not found.');
-      return replyButton;
-    }, {
-      interval: kOpenReplyEditorIntervalInMs,
-      timeout: kOpenReplyEditorTimeoutInMs,
-    });
-
-    const e = new Event('twpt-click');
-    replyButton.dispatchEvent(e);
-  }
-
-  async isFeatureEnabled() {
-    let options;
-    if (this.options !== null) {
-      options = this.options;
-    } else {
-      options =
-          await getOptions(['flattenthreads', 'flattenthreads_switch_enabled']);
-    }
-    return options['flattenthreads'] &&
-        options['flattenthreads_switch_enabled'];
-  }
-
-  getCurrentMessageId() {
-    const thread = parseUrl(window.location.href);
-    if (thread === false || thread.message === null) {
-      return null;
-    }
-    return thread.message;
-  }
-}
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();
 });