Avatars: show invisible icon when a thread isn't visible

When a thread isn't visible for anonymous users, we used to trigger an
error in the console. Now an icon is shown to users so they know avatars
couldn't be loaded due to the fact that the thread wasn't visible.

Fixed: twpowertools:105
Change-Id: If3808580ce322fe8e88ea45466725def277a60fc
diff --git a/src/contentScripts/communityConsole/avatars.js b/src/contentScripts/communityConsole/avatars.js
index 30b55d0..41ccd1c 100644
--- a/src/contentScripts/communityConsole/avatars.js
+++ b/src/contentScripts/communityConsole/avatars.js
@@ -82,8 +82,10 @@
 
   // 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. It also returns whether the thread is private, in which case the
-  // previous properties will be missing.
+  // thread.
+  //
+  // It also returns |state| which can be 'ok', 'private' or 'notVisible'. If it
+  // is 'private' or 'notVisible', the previous properties will be missing.
   getFirstMessages(thread, num = 15) {
     return CCApi(
                'ViewThread', {
@@ -115,16 +117,23 @@
           if (response.unauthorized)
             return this.db.putUnauthorizedForum(thread.forum).then(() => {
               return {
-                isPrivate: true,
+                state: 'private',
               };
             });
 
           var data = response.body;
 
           var numMessages = data?.['1']?.['8'];
-          if (numMessages === undefined)
-            throw new Error(
-                'Request to view thread doesn\'t include the number of messages');
+          if (numMessages === undefined) {
+            if (data?.['1']?.['10'] === false) {
+              return {
+                state: 'notVisible',
+              };
+            } else {
+              throw new Error(
+                  'Request to view thread doesn\'t include the number of messages');
+            }
+          }
 
           var messages = numMessages == 0 ? [] : data?.['1']['3'];
           if (messages === undefined)
@@ -138,7 +147,7 @@
                 'Author isn\'t included in the ViewThread response.');
 
           return {
-            isPrivate: false,
+            state: 'ok',
             messages,
             author,
 
@@ -151,14 +160,15 @@
   }
 
   // Get the following data:
-  // - |isPrivate|: whether the thread is private.
+  // - |state|: the state of the request (can be 'ok', 'private' or
+  // 'notVisible').
   // - |avatars|: a list of at most |num| avatars for thread |thread| by calling
-  // the API, if |isPrivate| is false.
+  // the API, if |state| is 'ok'.
   getVisibleAvatarsFromServer(thread, num) {
     return this.getFirstMessages(thread).then(result => {
-      if (result.isPrivate)
+      if (result.state != 'ok')
         return {
-          isPrivate: true,
+          state: result.state,
         };
 
       var messages = result.messages;
@@ -189,7 +199,7 @@
         });
 
       return {
-        isPrivate: false,
+        state: 'ok',
         avatars: avatarUrls,
       };
     });
@@ -260,8 +270,9 @@
   }
 
   // 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).
+  // - |state|: 'ok' (the avatars list could be retrieved), 'private' (the
+  // thread is in a private forum, so the avatars list could not be retrieved),
+  // or 'notVisible' (the thread has the visible field set to false).
   // - |avatars|: list of at most |num| avatars for thread |thread|
   getVisibleAvatars(thread, num = 3) {
     return this.isPrivateThread(thread).then(isPrivate => {
@@ -292,9 +303,9 @@
                   err);
 
             return this.getVisibleAvatarsFromServer(thread, num).then(res => {
-              if (res.isPrivate)
+              if (res.state != 'ok')
                 return {
-                  state: 'private',
+                  state: res.state,
                   avatars: [],
                 };
 
@@ -331,10 +342,11 @@
           var avatarUrls = res.avatars;
 
           let singleAvatar;
-          if (res.state == 'private') {
+          if (res.state == 'private' || res.state == 'notVisible') {
             singleAvatar = document.createElement('div');
             singleAvatar.classList.add('TWPT-avatar-private-placeholder');
-            singleAvatar.textContent = 'person_off';
+            singleAvatar.textContent =
+                (res.state == 'private' ? 'person_off' : 'visibility_off');
             avatarsContainer.appendChild(singleAvatar);
           } else {
             for (var i = 0; i < avatarUrls.length; ++i) {
@@ -352,6 +364,11 @@
                 'inject_threadlistavatars_private_thread_indicator_label');
             createPlainTooltip(singleAvatar, label);
           }
+          if (res.state == 'notVisible') {
+            var label = chrome.i18n.getMessage(
+                'inject_threadlistavatars_invisible_thread_indicator_label');
+            createPlainTooltip(singleAvatar, label);
+          }
         })
         .catch(err => {
           console.error(
diff --git a/src/static/_locales/en/messages.json b/src/static/_locales/en/messages.json
index f64219d..b78c8cc 100644
--- a/src/static/_locales/en/messages.json
+++ b/src/static/_locales/en/messages.json
@@ -283,6 +283,10 @@
     "message": "Due to technical reasons, we can't load the avatars of threads published in private forums.",
     "description": "Helper text which appears when hovering an icon next to a thread, to explain its meaning."
   },
+  "inject_threadlistavatars_invisible_thread_indicator_label": {
+    "message": "Due to technical reasons, we can't load the avatars of threads not visible to anonymous users.",
+    "description": "Helper text which appears when hovering an icon next to a thread, to explain its meaning."
+  },
   "inject_workflows_menubtn": {
     "message": "Run a workflow...",
     "description": "Tooltip of the icon shown above a thread or in thread lists when selecting multiple threads in the Community Console which lets the user show a menu with the worklofws they can run."