fix(extra-info): show extra info in the RCE thread page

The extra info feature worked with the old thread pages. This CL brings
support to the new RCE thread pages.

Bug: twpowertools:93
Change-Id: I47e4235afa4f7ec441f5a92edfcc28b1cb5f0419
diff --git a/src/contentScripts/communityConsole/extraInfo/services/message.js b/src/contentScripts/communityConsole/extraInfo/services/message.js
new file mode 100644
index 0000000..3b43403
--- /dev/null
+++ b/src/contentScripts/communityConsole/extraInfo/services/message.js
@@ -0,0 +1,56 @@
+import MessageModel from '../../../../models/Message.js';
+
+import StatesExtraInfoService from './states.js';
+
+export default class MessageExtraInfoService {
+  static getMessageIdFromNode(messageNode) {
+    const id =
+        messageNode.querySelector('.scTailwindThreadMessageMessagecardcontent')
+            ?.getAttribute?.('data-stats-id');
+    if (id === undefined)
+      throw new Error(`Couldn't retrieve message id from node.`);
+    return id;
+  }
+
+  static getMessageFromThreadModel(messageId, threadModel) {
+    for (const messageOrGap of threadModel.getMessageOrGapModels()) {
+      if (!(messageOrGap instanceof MessageModel)) continue;
+      if (messageOrGap.getId() == messageId) {
+        return messageOrGap;
+      } else {
+        for (const subMessageOrGap of messageOrGap.getCommentsAndGaps()) {
+          if (!(subMessageOrGap instanceof MessageModel)) continue;
+          if (subMessageOrGap.getId() == messageId) {
+            return subMessageOrGap;
+          }
+        }
+      }
+    }
+
+    throw new Error(`Couldn't find message ${messageId} in thread.`);
+  }
+
+  static getMessageChips(messageModel) {
+    const chips = [];
+    const tooltips = [];
+
+    const endPendingStateTimestampMicros =
+        messageModel.getEndPendingStateTimestampMicros();
+    const [pendingStateChip, pendingStateTooltip] =
+        StatesExtraInfoService.getPendingStateChip(
+            endPendingStateTimestampMicros);
+    if (pendingStateChip) chips.push(pendingStateChip);
+    if (pendingStateTooltip) tooltips.push(pendingStateTooltip);
+
+    const itemMetadata = messageModel.data?.[1]?.[5];
+    chips.push(...StatesExtraInfoService.getMetadataChips(itemMetadata));
+
+    const liveReviewStatus = messageModel.data?.[1]?.[36];
+    const [liveReviewChip, liveReviewTooltip] =
+        StatesExtraInfoService.getLiveReviewStatusChip(liveReviewStatus);
+    if (liveReviewChip) chips.push(liveReviewChip);
+    if (liveReviewTooltip) tooltips.push(liveReviewTooltip);
+
+    return [chips, tooltips];
+  }
+}
diff --git a/src/contentScripts/communityConsole/extraInfo/services/states.js b/src/contentScripts/communityConsole/extraInfo/services/states.js
new file mode 100644
index 0000000..6904e3e
--- /dev/null
+++ b/src/contentScripts/communityConsole/extraInfo/services/states.js
@@ -0,0 +1,106 @@
+import {createPlainTooltip} from '../../../../common/tooltip.js';
+import {kItemMetadataState, kItemMetadataStateI18n} from '../consts.js';
+
+export default class StatesExtraInfoService {
+  static getPendingStateChip(endPendingStateTimestampMicros) {
+    const endPendingStateTimestamp =
+        Math.floor(endPendingStateTimestampMicros / 1e3);
+    const now = Date.now();
+    if (!endPendingStateTimestampMicros || endPendingStateTimestamp < now)
+      return [null, null];
+
+    const span = document.createElement('span');
+    span.textContent =
+        chrome.i18n.getMessage('inject_extrainfo_message_pendingstate');
+
+    const date = new Date(endPendingStateTimestamp).toLocaleString();
+    const pendingTooltip = createPlainTooltip(
+        span,
+        chrome.i18n.getMessage(
+            'inject_extrainfo_message_pendingstate_tooltip', [date]),
+        false);
+    return [span, pendingTooltip];
+  }
+
+  static getLiveReviewStatusChip(liveReviewStatus) {
+    const verdict = liveReviewStatus?.['1'];
+    if (!verdict) return [null, null];
+
+    const [label, labelClass] = this.getLiveReviewStatusLabel(verdict);
+    if (!label || !labelClass) return [null, null];
+
+    const reviewedBy = liveReviewStatus?.['2'];
+    const timestamp = liveReviewStatus?.['3'];
+    const date = (new Date(Math.floor(timestamp / 1e3))).toLocaleString();
+
+    let a = document.createElement('a');
+    a.href = 'https://support.google.com/s/community/user/' + reviewedBy;
+    a.classList.add(labelClass);
+    a.textContent = chrome.i18n.getMessage(
+        'inject_extrainfo_message_livereviewverdict',
+        [chrome.i18n.getMessage(
+            'inject_extrainfo_message_livereviewverdict_' + label)]);
+    let liveReviewTooltip = createPlainTooltip(a, date, false);
+    return [a, liveReviewTooltip];
+  }
+
+  static getLiveReviewStatusLabel(verdict) {
+    let label, labelClass;
+    switch (verdict) {
+      case 1:  // LIVE_REVIEW_RELEVANT
+        label = 'relevant';
+        labelClass = 'TWPT-extrainfo-good';
+        break;
+
+      case 2:  // LIVE_REVIEW_OFF_TOPIC
+        label = 'offtopic';
+        labelClass = 'TWPT-extrainfo-bad';
+        break;
+
+      case 3:  // LIVE_REVIEW_ABUSE
+        label = 'abuse';
+        labelClass = 'TWPT-extrainfo-bad';
+        break;
+
+      default:
+        return [null, null];
+    }
+    return [label, labelClass];
+  }
+
+  static getMetadataChips(itemMetadata) {
+    return [
+      this.getStateChip(itemMetadata),
+      this.getShadowBlockChip(itemMetadata),
+    ].filter(chip => chip !== null);
+  }
+
+  static getStateChip(itemMetadata) {
+    const state = itemMetadata?.['1'];
+    if (!state || state == 1) return null;
+
+    const stateI18nKey =
+        'inject_extrainfo_message_state_' + kItemMetadataStateI18n[state];
+    const stateLocalized = chrome.i18n.getMessage(stateI18nKey) ?? state;
+
+    const span = document.createElement('span');
+    span.textContent = chrome.i18n.getMessage(
+        'inject_extrainfo_message_state', [stateLocalized]);
+    span.title = kItemMetadataState[state] ?? state;
+    return span;
+  }
+
+  static getShadowBlockChip(itemMetadata) {
+    const shadowBlockInfo = itemMetadata?.['10'];
+    const blockedTimestampMicros = shadowBlockInfo?.['2'];
+    if (!blockedTimestampMicros) return null;
+
+    const isBlocked = shadowBlockInfo?.['1'];
+    let span = document.createElement('span');
+    span.textContent = chrome.i18n.getMessage(
+        'inject_extrainfo_message_shadowblock' +
+        (isBlocked ? 'active' : 'notactive'));
+    if (isBlocked) span.classList.add('TWPT-extrainfo-bad');
+    return span;
+  }
+}
diff --git a/src/contentScripts/communityConsole/extraInfo/services/thread.js b/src/contentScripts/communityConsole/extraInfo/services/thread.js
index 357ed3d..c36dfa5 100644
--- a/src/contentScripts/communityConsole/extraInfo/services/thread.js
+++ b/src/contentScripts/communityConsole/extraInfo/services/thread.js
@@ -1,5 +1,4 @@
-import {createPlainTooltip} from '../../../../common/tooltip.js';
-import {kItemMetadataState, kItemMetadataStateI18n} from '../consts.js';
+import StatesExtraInfoService from './states.js';
 
 export default class ThreadExtraInfoService {
   /**
@@ -11,42 +10,27 @@
     let chips = [];
     let tooltips = [];
 
-    const [pendingStateInfo, pendingTooltip] = this.getPendingStateChip(thread);
+    const endPendingStateTimestampMicros = thread?.['2']?.['39'];
+    const [pendingStateInfo, pendingTooltip] =
+        StatesExtraInfoService.getPendingStateChip(
+            endPendingStateTimestampMicros);
     if (pendingStateInfo) chips.push(pendingStateInfo);
     if (pendingTooltip) tooltips.push(pendingTooltip);
 
     chips.push(...this.getTrendingChips(thread));
-    chips.push(...this.getMetadataChips(thread));
 
+    const itemMetadata = thread?.['2']?.['12'];
+    chips.push(...StatesExtraInfoService.getMetadataChips(itemMetadata));
+
+    const liveReviewStatus = thread?.['2']?.['38'];
     const [liveReviewInfo, liveReviewTooltip] =
-        this.getLiveReviewStatusChip(thread);
+        StatesExtraInfoService.getLiveReviewStatusChip(liveReviewStatus);
     if (liveReviewInfo) chips.push(liveReviewInfo);
     if (liveReviewTooltip) tooltips.push(liveReviewTooltip);
 
     return [chips, tooltips];
   }
 
-  static getPendingStateChip(thread) {
-    const endPendingStateTimestampMicros = thread?.['2']?.['39'];
-    const endPendingStateTimestamp =
-        Math.floor(endPendingStateTimestampMicros / 1e3);
-    const now = Date.now();
-    if (!endPendingStateTimestampMicros || endPendingStateTimestamp < now)
-      return [null, null];
-
-    const span = document.createElement('span');
-    span.textContent =
-        chrome.i18n.getMessage('inject_extrainfo_message_pendingstate');
-
-    const date = new Date(endPendingStateTimestamp).toLocaleString();
-    const pendingTooltip = createPlainTooltip(
-        span,
-        chrome.i18n.getMessage(
-            'inject_extrainfo_message_pendingstate_tooltip', [date]),
-        false);
-    return [span, pendingTooltip];
-  }
-
   static getTrendingChips(thread) {
     const chips = [];
 
@@ -62,91 +46,6 @@
     return chips;
   }
 
-  static getMetadataChips(thread) {
-    const itemMetadata = thread?.['2']?.['12'];
-
-    return [
-      this.getStateChip(itemMetadata),
-      this.getShadowBlockChip(itemMetadata),
-    ].filter(chip => chip !== null);
-  }
-
-  static getLiveReviewStatusChip(thread) {
-    const liveReviewStatus = thread?.['2']?.['38'];
-    const verdict = liveReviewStatus?.['1'];
-    if (!verdict) return [null, null];
-
-    const [label, labelClass] = this.getLiveReviewStatusLabel(verdict);
-    if (!label || !labelClass) return [null, null];
-
-    const reviewedBy = liveReviewStatus?.['2'];
-    const timestamp = liveReviewStatus?.['3'];
-    const date = (new Date(Math.floor(timestamp / 1e3))).toLocaleString();
-
-    let a = document.createElement('a');
-    a.href = 'https://support.google.com/s/community/user/' + reviewedBy;
-    a.classList.add(labelClass);
-    a.textContent = chrome.i18n.getMessage(
-        'inject_extrainfo_message_livereviewverdict',
-        [chrome.i18n.getMessage(
-            'inject_extrainfo_message_livereviewverdict_' + label)]);
-    let liveReviewTooltip = createPlainTooltip(a, date, false);
-    return [a, liveReviewTooltip];
-  }
-
-  static getStateChip(itemMetadata) {
-    const state = itemMetadata?.['1'];
-    if (!state || state == 1) return null;
-
-    const stateI18nKey =
-        'inject_extrainfo_message_state_' + kItemMetadataStateI18n[state];
-    const stateLocalized = chrome.i18n.getMessage(stateI18nKey) ?? state;
-
-    const span = document.createElement('span');
-    span.textContent = chrome.i18n.getMessage(
-        'inject_extrainfo_message_state', [stateLocalized]);
-    span.title = kItemMetadataState[state] ?? state;
-    return span;
-  }
-
-  static getLiveReviewStatusLabel(verdict) {
-    let label, labelClass;
-    switch (verdict) {
-      case 1:  // LIVE_REVIEW_RELEVANT
-        label = 'relevant';
-        labelClass = 'TWPT-extrainfo-good';
-        break;
-
-      case 2:  // LIVE_REVIEW_OFF_TOPIC
-        label = 'offtopic';
-        labelClass = 'TWPT-extrainfo-bad';
-        break;
-
-      case 3:  // LIVE_REVIEW_ABUSE
-        label = 'abuse';
-        labelClass = 'TWPT-extrainfo-bad';
-        break;
-
-      default:
-        return [null, null];
-    }
-    return [label, labelClass];
-  }
-
-  static getShadowBlockChip(itemMetadata) {
-    const shadowBlockInfo = itemMetadata?.['10'];
-    const blockedTimestampMicros = shadowBlockInfo?.['2'];
-    if (!blockedTimestampMicros) return null;
-
-    const isBlocked = shadowBlockInfo?.['1'];
-    let span = document.createElement('span');
-    span.textContent = chrome.i18n.getMessage(
-        'inject_extrainfo_message_shadowblock' +
-        (isBlocked ? 'active' : 'notactive'));
-    if (isBlocked) span.classList.add('TWPT-extrainfo-bad');
-    return span;
-  }
-
   static getThreadFromThreadList(threadList, currentThreadInfo) {
     return threadList?.find?.(thread => {
       const threadInfo = thread?.['2']?.['1'];