ThreadListAvatars: add private thread indicator

When a thread belongs to a private forum, we can't obtain its avatars.
This changes makes this clear by inserting a "key" icon where the
avatars should be shown.

Bug: twpowertools:30
Change-Id: Idec33f277b12282df0fd271eebe0156865474bf4
diff --git a/src/contentScripts/communityConsole/avatars.js b/src/contentScripts/communityConsole/avatars.js
index d0a91df..093e217 100644
--- a/src/contentScripts/communityConsole/avatars.js
+++ b/src/contentScripts/communityConsole/avatars.js
@@ -54,19 +54,19 @@
   // get its avatars since it makes an anonymomus call to get the contents of
   // the thread.
   //
-  // This function returns whether avatars should be retrieved depending on if
-  // the thread belongs to a known private forum.
-  shouldRetrieveAvatars(thread) {
+  // This function returns whether the thread belongs to a known private forum.
+  isPrivateThread(thread) {
     return this.getPrivateForums().then(privateForums => {
-      if (privateForums.includes(thread.forum)) return false;
+      if (privateForums.includes(thread.forum)) return true;
 
-      return this.db.isForumUnauthorized(thread.forum).then(res => !res);
+      return this.db.isForumUnauthorized(thread.forum);
     });
   }
 
   // Get an object with the author of the thread, an array of the first |num|
   // replies from the thread |thread|, and additional information about the
-  // thread.
+  // thread. It also returns whether the thread is private, in which case the
+  // previous properties will be missing.
   getFirstMessages(thread, num = 15) {
     return CCApi(
                'ViewThread', {
@@ -97,7 +97,9 @@
         .then(response => {
           if (response.unauthorized)
             return this.db.putUnauthorizedForum(thread.forum).then(() => {
-              throw new Error('Permission denied to load thread.');
+              return {
+                isPrivate: true,
+              };
             });
 
           var data = response.body;
@@ -119,6 +121,7 @@
                 'Author isn\'t included in the ViewThread response.');
 
           return {
+            isPrivate: false,
             messages,
             author,
 
@@ -130,9 +133,17 @@
         });
   }
 
-  // Get a list of at most |num| avatars for thread |thread| by calling the API
+  // Get the following data:
+  // - |isPrivate|: whether the thread is private.
+  // - |avatars|: a list of at most |num| avatars for thread |thread| by calling
+  // the API, if |isPrivate| is false.
   getVisibleAvatarsFromServer(thread, num) {
     return this.getFirstMessages(thread).then(result => {
+      if (result.isPrivate)
+        return {
+          isPrivate: true,
+        };
+
       var messages = result.messages;
       var author = result.author;
       var lastMessageId = result.lastMessageId;
@@ -160,7 +171,10 @@
           lastUsedTimestamp: Math.floor(Date.now() / 1000),
         });
 
-      return avatarUrls;
+      return {
+        isPrivate: false,
+        avatars: avatarUrls,
+      };
     });
   }
 
@@ -228,13 +242,17 @@
         });
   }
 
-  // Get a list of at most |num| avatars for thread |thread|
+  // Get an object with the following data:
+  // - |state|: 'ok' (the avatars list could be retrieved) or 'private' (the
+  // thread is private, so the avatars list could not be retrieved).
+  // - |avatars|: list of at most |num| avatars for thread |thread|
   getVisibleAvatars(thread, num = 3) {
-    return this.shouldRetrieveAvatars(thread).then(shouldRetrieve => {
-      if (!shouldRetrieve) {
-        console.debug('[threadListAvatars] Skipping thread', thread);
-        return [];
-      }
+    return this.isPrivateThread(thread).then(isPrivate => {
+      if (isPrivate)
+        return {
+          state: 'private',
+          avatars: [],
+        };
 
       return this.getVisibleAvatarsFromCacheAfterInvalidations(thread, num)
           .then(res => {
@@ -243,7 +261,10 @@
               err.name = 'notCached';
               throw err;
             }
-            return res.entry.avatarUrls;
+            return {
+              state: 'ok',
+              avatars: res.entry.avatarUrls,
+            };
           })
           .catch(err => {
             // If the name is "notCached", then this is not an actual error so
@@ -253,7 +274,18 @@
                   '[threadListAvatars] Error while accessing avatars cache:',
                   err);
 
-            return this.getVisibleAvatarsFromServer(thread, num);
+            return this.getVisibleAvatarsFromServer(thread, num).then(res => {
+              if (res.isPrivate)
+                return {
+                  state: 'private',
+                  avatars: [],
+                };
+
+              return {
+                state: 'ok',
+                avatars: res.avatars,
+              };
+            });
           });
     });
   }
@@ -275,17 +307,24 @@
     }
 
     this.getVisibleAvatars(thread)
-        .then(avatarUrls => {
+        .then(res => {
           var avatarsContainer = document.createElement('div');
           avatarsContainer.classList.add('TWPT-avatars');
 
-          var count = Math.floor(Math.random() * 4);
+          var avatarUrls = res.avatars;
 
-          for (var i = 0; i < avatarUrls.length; ++i) {
+          if (res.state == 'private') {
             var avatar = document.createElement('div');
-            avatar.classList.add('TWPT-avatar');
-            avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
+            avatar.classList.add('TWPT-avatar-private-placeholder');
+            avatar.textContent = 'vpn_key';
             avatarsContainer.appendChild(avatar);
+          } else {
+            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);
diff --git a/src/static/css/thread_list_avatars.css b/src/static/css/thread_list_avatars.css
index 2dcb29b..beacbf1 100644
--- a/src/static/css/thread_list_avatars.css
+++ b/src/static/css/thread_list_avatars.css
@@ -6,19 +6,32 @@
   margin-inline-start: 14px;
 }
 
-.TWPT-avatars .TWPT-avatar {
+.TWPT-avatars .TWPT-avatar,
+    .TWPT-avatars .TWPT-avatar-private-placeholder {
   height: 28px;
   width: 28px;
   align-self: center;
   border-width: 0;
   border-radius: 50%;
   margin-inline-start: 6px;
+}
+
+.TWPT-avatars .TWPT-avatar {
   background-color: white;
   background-position: center;
   background-size: contain;
   background-repeat: no-repeat;
 }
 
+.TWPT-avatars .TWPT-avatar-private-placeholder {
+  line-height: 28px;
+  text-align: center;
+  color: #35363a;
+  background-color: #dadce0;
+  font-size: 20px;
+  font-family: 'Google Material Icons';
+}
+
 /*
  * Changing styles of existing elements so the avatars fit.
  */