Add threadListAvatars experiment

This experiment adds avatars of the users who have participated in a
thread in the thread list, next to each thread.

Change-Id: I259b103a7d3462201013ab2027866bbcce476901
diff --git a/src/_locales/ca/messages.json b/src/_locales/ca/messages.json
index 2c3ed92..9bf0e5c 100644
--- a/src/_locales/ca/messages.json
+++ b/src/_locales/ca/messages.json
@@ -99,6 +99,10 @@
     "message": "Posa el botó \"expandir fil\" a l'esquerra del tot en les llistes de fils de la Consola de la Comunitat.",
     "description": "Feature checkbox in the options page"
   },
+  "options_threadlistavatars": {
+    "message": "Mostra fotos de perfil a les llistes de fils de la Consola de la Comunitat.",
+    "description": "Feature checkbox in the options page"
+  },
   "options_profileindicator_header": {
     "message": "Punt indicador",
     "description": "Heading for the profile indicator feature options"
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 7c8b45b..b8fbd5b 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -103,6 +103,10 @@
     "message": "Place the \"expand thread\" button all the way to the left in the Community Console thread lists.",
     "description": "Feature checkbox in the options page"
   },
+  "options_threadlistavatars": {
+    "message": "Show avatars in thread lists in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
   "options_profileindicator_header": {
     "message": "Indicator dot",
     "description": "Heading for the profile indicator feature options"
diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json
index ae9fbdd..aec514e 100644
--- a/src/_locales/es/messages.json
+++ b/src/_locales/es/messages.json
@@ -103,6 +103,10 @@
     "message": "Punto indicador",
     "description": "Heading for the profile indicator feature options"
   },
+  "options_threadlistavatars": {
+    "message": "Muestra fotos de perfil en las listas de hilos de la Consola de la Comunidad.",
+    "description": "Feature checkbox in the options page"
+  },
   "options_profileindicator_moreinfo": {
     "message": "+info sobre las 2 opciones anteriores",
     "description": "Link to learn more about the profile indicator feature"
diff --git a/src/common/common.js b/src/common/common.js
index 205cdc9..5703dbc 100644
--- a/src/common/common.js
+++ b/src/common/common.js
@@ -82,7 +82,10 @@
   },
 
   // Experiments:
-
+  'threadlistavatars': {
+    defaultValue: false,
+    context: 'experiments',
+  },
 
   // Internal options:
   'ccdarktheme_switch_enabled': {
diff --git a/src/content_scripts/console_inject.js b/src/content_scripts/console_inject.js
index c042f92..ae10881 100644
--- a/src/content_scripts/console_inject.js
+++ b/src/content_scripts/console_inject.js
@@ -13,6 +13,20 @@
   return getNParent(node.parentNode, n - 1);
 }
 
+function parseUrl(url) {
+  var forum_a = url.match(/forum\/([0-9]+)/i);
+  var thread_a = url.match(/thread\/([0-9]+)/i);
+
+  if (forum_a === null || thread_a === null) {
+    return false;
+  }
+
+  return {
+    'forum': forum_a[1],
+    'thread': thread_a[1],
+  };
+}
+
 function createExtBadge() {
   var badge = document.createElement('div');
   badge.classList.add('TWPT-badge');
@@ -221,6 +235,93 @@
       clone, (readToggle.nextSibling || readToggle));
 }
 
+// TODO(avm99963): This is a prototype. DON'T FORGET TO ADD ERROR HANDLING.
+function injectAvatars(node) {
+  var header = node.querySelector(
+      'ec-thread-summary .main-header .panel-description a.header');
+  if (header === null) return;
+
+  var link = parseUrl(header.href);
+  if (link === false) return;
+
+  var APIRequestUrl = 'https://support.google.com/s/community/api/ViewThread' +
+      (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
+
+  fetch(APIRequestUrl, {
+    'headers': {
+      'content-type': 'text/plain; charset=utf-8',
+    },
+    'body': JSON.stringify({
+      1: link.forum,
+      2: link.thread,
+      3: {
+        1: {2: 15},
+        3: true,
+        5: true,
+        10: true,
+        16: true,
+        18: true,
+      }
+    }),
+    'method': 'POST',
+    'mode': 'cors',
+    'credentials': 'omit',
+  })
+      .then(res => {
+        if (res.status == 200 || res.status == 400) {
+          return res.json().then(data => ({
+                                   status: res.status,
+                                   body: data,
+                                 }));
+        } else {
+          throw new Error('Status code ' + res.status + ' was not expected.');
+        }
+      })
+      .then(res => {
+        if (res.status == 400) {
+          throw new Error(
+              res.body[4] ||
+              ('Response status: 400. Error code: ' + res.body[2]));
+        }
+
+        return res.body;
+      })
+      .then(data => {
+        if (!('1' in data) || !('8' in data['1'])) return false;
+
+        var messages = data['1']['8'];
+        if (messages == 0) return;
+
+        var avatarUrls = [];
+
+        if (!('3' in data['1'])) return false;
+        for (var m of data['1']['3']) {
+          if (!('3' in m) || !('1' in m['3']) || !('2' in m['3']['1']))
+            continue;
+
+          var url = m['3']['1']['2'];
+
+          if (!avatarUrls.includes(url)) avatarUrls.push(url);
+
+          if (avatarUrls.length == 3) break;
+        }
+
+        var avatarsContainer = document.createElement('div');
+        avatarsContainer.classList.add('TWPT-avatars');
+
+        var count = Math.floor(Math.random() * 4);
+
+        for (var i = 0; i < avatarUrls.length; ++i) {
+          var avatar = document.createElement('div');
+          avatar.classList.add('TWPT-avatar');
+          avatar.style.backgroundImage = 'url(\''+avatarUrls[i]+'\')';
+          avatarsContainer.appendChild(avatar);
+        }
+
+        header.appendChild(avatarsContainer);
+      });
+}
+
 function injectPreviousPostsLinks(nameElement) {
   var mainCardContent = getNParent(nameElement, 3);
   if (mainCardContent === null) {
@@ -272,6 +373,9 @@
   // Read/unread bulk action in the list of thread, for the batch lock feature
   '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)
+  'li',
 ];
 
 function handleCandidateNode(node) {
@@ -339,6 +443,13 @@
     if (options.batchlock && nodeIsReadToggleBtn(node)) {
       addBatchLockBtn(node);
     }
+
+    // Inject avatar links to threads in the thread list
+    if (options.threadlistavatars && ('tagName' in node) &&
+        (node.tagName == 'LI') &&
+        node.querySelector('ec-thread-summary') !== null) {
+      injectAvatars(node);
+    }
   }
 }
 
@@ -417,4 +528,9 @@
   if (options.batchlock) {
     injectScript(chrome.runtime.getURL('injections/batchlock_inject.js'));
   }
+
+  if (options.threadlistavatars) {
+    injectStylesheet(
+        chrome.runtime.getURL('injections/thread_list_avatars.css'));
+  }
 });
diff --git a/src/injections/thread_list_avatars.css b/src/injections/thread_list_avatars.css
new file mode 100644
index 0000000..a418005
--- /dev/null
+++ b/src/injections/thread_list_avatars.css
@@ -0,0 +1,27 @@
+.TWPT-avatars {
+  display: flex;
+  flex-direction: row-reverse;
+  width: 102px;
+  overflow-x: hidden;
+  margin-left: 8px;
+}
+
+.TWPT-avatars .TWPT-avatar {
+  height: 28px;
+  width: 28px;
+  align-self: center;
+  border-width: 0;
+  border-radius: 50%;
+  margin-left: 6px;
+  background-color: white;
+  background-position: center;
+  background-size: contain;
+  background-repeat: no-repeat;
+}
+
+/*
+ * Changing styles of existing elements so the avatars fit.
+ */
+ec-thread-summary .main-header .panel-description a.header .header-content {
+  width: calc(100% - 204px);
+}
diff --git a/src/options/experiments.html b/src/options/experiments.html
index a49e6c9..e25254b 100644
--- a/src/options/experiments.html
+++ b/src/options/experiments.html
@@ -12,6 +12,7 @@
       <h1 data-i18n="experiments_title"></h1>
       <p data-i18n="experiments_description"></p>
       <form>
+        <div class="option"><input type="checkbox" id="threadlistavatars"> <label for="threadlistavatars" data-i18n="threadlistavatars"></label></div>
         <div class="actions"><button id="save" data-i18n="save"></button></div>
       </form>
       <div id="save-indicator"></div>