extraInfo: show extra info in the canned responses list

Bug: twpowertools:93
Change-Id: I8d81391cb931f2096c704b50ff4f3cc9037b8c6e
diff --git a/src/common/xhrInterceptors.json5 b/src/common/xhrInterceptors.json5
index 2e9a443..126b6fb 100644
--- a/src/common/xhrInterceptors.json5
+++ b/src/common/xhrInterceptors.json5
@@ -20,5 +20,10 @@
       urlRegex: "api/ViewUnifiedUser",
       intercepts: "response",
     },
+    {
+      eventName: "ListCannedResponsesResponse",
+      urlRegex: "api/ListCannedResponses",
+      intercepts: "response",
+    },
   ],
 }
diff --git a/src/contentScripts/communityConsole/extraInfo.js b/src/contentScripts/communityConsole/extraInfo.js
index c572523..266592a 100644
--- a/src/contentScripts/communityConsole/extraInfo.js
+++ b/src/contentScripts/communityConsole/extraInfo.js
@@ -2,10 +2,12 @@
 import {waitFor} from 'poll-until-promise';
 
 import {isOptionEnabled} from '../../common/optionsUtils.js';
+import {createPlainTooltip} from '../../common/tooltip.js';
 
 import {createExtBadge} from './utils/common.js';
 
 const kViewUnifiedUserResponseEvent = 'TWPT_ViewUnifiedUserResponse';
+const kListCannedResponsesResponse = 'TWPT_ListCannedResponsesResponse';
 
 const kAbuseCategories = [
   ['1', 'Account'],
@@ -147,6 +149,11 @@
       id: -1,
       timestamp: 0,
     };
+    this.lastCRsList = {
+      body: {},
+      id: -1,
+      duplicateNames: new Set(),
+    };
     this.setUpHandlers();
   }
 
@@ -160,6 +167,27 @@
         timestamp: Date.now(),
       };
     });
+    window.addEventListener(kListCannedResponsesResponse, e => {
+      if (e.detail.id < this.lastCRsList.id) return;
+
+      // Look if there are duplicate names
+      const crs = e.detail.body?.['1'] ?? [];
+      const names = crs.map(cr => cr?.['7']).slice().sort();
+      let duplicateNames = new Set();
+      for (let i = 1; i < names.length; i++)
+        if (names[i - 1] == names[i]) duplicateNames.add(names[i]);
+
+      this.lastCRsList = {
+        body: e.detail.body,
+        id: e.detail.id,
+        duplicateNames,
+      };
+    });
+  }
+
+  // Whether the feature is enabled
+  isEnabled() {
+    return isOptionEnabled('extrainfo');
   }
 
   // Add a pretty component which contains |info| to |node|.
@@ -173,8 +201,7 @@
     let badgeCell = document.createElement('div');
     badgeCell.classList.add('TWPT-extrainfo-badge-cell');
 
-    let badge, badgeTooltip;
-    [badge, badgeTooltip] = createExtBadge();
+    const [badge, badgeTooltip] = createExtBadge();
     badgeCell.append(badge);
 
     let infoCell = document.createElement('div');
@@ -250,8 +277,79 @@
   }
 
   injectAtProfileIfEnabled(card) {
-    isOptionEnabled('extrainfo').then(isEnabled => {
+    this.isEnabled().then(isEnabled => {
       if (isEnabled) return this.injectAtProfile(card);
     });
   }
+
+  // Canned responses (CRs) functionality
+
+  getCRName(tags, isExpanded) {
+    if (!isExpanded)
+      return tags.parentNode?.querySelector?.('.text .name')?.textContent;
+
+    // https://www.youtube.com/watch?v=Z6_ZNW1DACE
+    return tags.parentNode?.parentNode?.parentNode?.parentNode?.parentNode
+        ?.parentNode?.parentNode?.querySelector?.('.text .name')
+        ?.textContent;
+  }
+
+  // Inject usage stats in the |tags| component of a CR
+  injectAtCR(tags, isExpanded) {
+    waitFor(() => {
+      if (this.lastCRsList.id != -1) return Promise.resolve(this.lastCRsList);
+      return Promise.reject('Didn\'t receive canned responses list');
+    }, {
+      interval: 500,
+      timeout: 15 * 1000,
+    }).then(crs => {
+      let name = this.getCRName(tags, isExpanded);
+
+      // If another CR has the same name, there's no easy way to distinguish
+      // them, so don't show the usage stats.
+      if (crs.duplicateNames.has(name)) return;
+
+      for (const cr of (crs.body?.['1'] ?? [])) {
+        if (cr['7'] == name) {
+          let tag = document.createElement('material-chip');
+          tag.classList.add('TWPT-tag');
+
+          let container = document.createElement('div');
+          container.classList.add('TWPT-chip-content-container');
+
+          let content = document.createElement('div');
+          content.classList.add('TWPT-content');
+
+          const [badge, badgeTooltip] = createExtBadge();
+
+          let label = document.createElement('span');
+          label.textContent = 'Used ' + (cr['8'] ?? '0') + ' times';
+
+          content.append(badge, label);
+          container.append(content);
+          tag.append(container);
+          tags.append(tag);
+
+          new MDCTooltip(badgeTooltip);
+
+          if (cr['9']) {
+            const lastUsedTime = Math.floor(parseInt(cr['9']) / 1e3);
+            let date = (new Date(lastUsedTime)).toLocaleString();
+            createPlainTooltip(label, 'Last used: ' + date);
+          }
+
+          break;
+        }
+      }
+    });
+  }
+
+  injectAtCRIfEnabled(tags, isExpanded) {
+    // If the tag has already been injected, exit.
+    if (tags.querySelector('.TWPT-tag')) return;
+
+    this.isEnabled().then(isEnabled => {
+      if (isEnabled) return this.injectAtCR(tags, isExpanded);
+    });
+  }
 }
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index a534f38..73ed360 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -44,6 +44,10 @@
 
   // Unified profile iframe
   'iframe',
+
+  // Canned response tags or toolbelt (for the extra info feature)
+  '.tags',
+  '.toolbelt',
 ];
 
 function handleCandidateNode(node) {
@@ -149,6 +153,15 @@
         unifiedProfilesFix.checkIframe(node)) {
       unifiedProfilesFix.fixIframe(node);
     }
+
+    // Show additional details in the canned responses view.
+    if (node.matches('ec-canned-response-row .tags')) {
+      window.TWPTExtraInfo.injectAtCRIfEnabled(node, /* isExpanded = */ false);
+    }
+    if (node.matches('ec-canned-response-row .main .toolbelt')) {
+      const tags = node.parentNode?.querySelector?.('.tags');
+      if (tags) window.TWPTExtraInfo.injectAtCRIfEnabled(tags, /* isExpanded = */ true);
+    }
   }
 }
 
diff --git a/src/static/_locales/en/messages.json b/src/static/_locales/en/messages.json
index 2637808..0e765ec 100644
--- a/src/static/_locales/en/messages.json
+++ b/src/static/_locales/en/messages.json
@@ -152,7 +152,7 @@
     "description": "Button in the options page which opens the workflow management page."
   },
   "options_extrainfo": {
-    "message": "Show extra information in threads and profiles.",
+    "message": "Show additional information in threads, profiles and the canned responses list.",
     "description": "Feature checkbox in the options page"
   },
   "options_save": {
diff --git a/src/static/css/ccdarktheme.css b/src/static/css/ccdarktheme.css
index aa49585..b8ea787 100644
--- a/src/static/css/ccdarktheme.css
+++ b/src/static/css/ccdarktheme.css
@@ -1502,7 +1502,8 @@
 
 ec-canned-responses .label-row,
     ec-canned-responses ec-canned-response-row .snippet,
-    ec-canned-responses ec-canned-response-row .tag {
+    ec-canned-responses ec-canned-response-row .tag .content,
+    ec-canned-responses ec-canned-response-row .TWPT-tag .TWPT-content {
   color: var(--TWPT-secondary-text)!important;
 }
 
diff --git a/src/static/css/extrainfo.css b/src/static/css/extrainfo.css
index 86eed01..d04d42d 100644
--- a/src/static/css/extrainfo.css
+++ b/src/static/css/extrainfo.css
@@ -32,3 +32,38 @@
   margin-top: 16px;
   align-self: end;
 }
+
+/* Special tags components for canned responses */
+ec-canned-response-row .TWPT-tag {
+  background-color: transparent;
+  color: #474747;
+  margin: 0 0 0 4px;
+  border: 1px solid #ababab;
+  display: flex;
+  align-items: center;
+  border-radius: 16px;
+  height: 32px;
+  overflow: hidden;
+}
+
+ec-canned-response-row .TWPT-chip-content-container {
+  margin: 0 12px;
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+ec-canned-response-row .TWPT-content {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: flex;
+  align-items: center;
+}
+
+ec-canned-response-row .TWPT-content .TWPT-badge {
+  --icon-size: 14px;
+  margin-right: 6px;
+}
+
+ec-canned-response-row .TWPT-content span[aria-describedby] {
+  cursor: help;
+}