Flatten threads: don't show reply button when appropriate

This CL also refactors the response interceptors to use the new
ThreadModel.

Fixed: twpowertools:160
Change-Id: I859e0fa1b8f5f4057bd66af3d167e4b21c6d12ed
diff --git a/src/models/Message.js b/src/models/Message.js
index 7aae392..05dc795 100644
--- a/src/models/Message.js
+++ b/src/models/Message.js
@@ -1,9 +1,11 @@
+import ItemMetadataState from './enums/ItemMetadataState.js';
 import GapModel from './Gap.js';
 import ThreadModel from './Thread.js';
 
 export default class MessageModel {
-  constructor(data) {
+  constructor(data, thread) {
     this.data = data ?? {};
+    this.thread = thread ?? new ThreadModel();
     this.commentsAndGaps = null;
   }
 
@@ -21,10 +23,17 @@
     return this.data[12] ?? [];
   }
 
+  #getMessageOrGapModels() {
+    const rawMogs = this.getRawCommentsAndGaps();
+    return rawMogs.filter(mog => mog !== undefined).map(mog => {
+      if (mog[1]) return new MessageModel(mog[1], this.thread);
+      if (mog[2]) return new GapModel(mog[2], this.thread);
+    });
+  }
+
   getCommentsAndGaps() {
     if (this.commentsAndGaps === null)
-      this.commentsAndGaps =
-          MessageModel.mapToMessageOrGapModels(this.getRawCommentsAndGaps());
+      this.commentsAndGaps = this.#getMessageOrGapModels();
     return this.commentsAndGaps;
   }
 
@@ -59,6 +68,24 @@
     delete this.data[1][37];
   }
 
+  isDeleted() {
+    return this.data[5]?.[3] ?? null;
+  }
+
+  getState() {
+    return this.data[5]?.[1] ?? null;
+  }
+
+  isTakenDown() {
+    return [
+      ItemMetadataState.AUTOMATED_ABUSE_TAKE_DOWN_DELETE,
+      ItemMetadataState.MANUAL_PROFILE_TAKE_DOWN_SUSPEND,
+      ItemMetadataState.AUTOMATED_ABUSE_TAKE_DOWN_HIDE,
+      ItemMetadataState.MANUAL_TAKE_DOWN_DELETE,
+      ItemMetadataState.MANUAL_TAKE_DOWN_HIDE,
+    ].includes(this.getState());
+  }
+
   isComment() {
     return !!this.getParentMessageId;
   }
@@ -67,16 +94,31 @@
     return {1: this.data};
   }
 
-  static mapToMessageOrGapModels(rawArray) {
-    return rawArray.filter(mog => mog !== undefined).map(mog => {
-      if (mog[1]) return new MessageModel(mog[1]);
-      if (mog[2]) return new GapModel(mog[2]);
-    });
-  }
-
   mergeCommentOrGapViews(a) {
     this.commentsAndGaps = ThreadModel.mergeMessageOrGaps(
         a.getCommentsAndGaps(), this.getCommentsAndGaps());
     this.data[12] = this.commentsAndGaps.map(cog => cog.toRawMessageOrGap());
   }
+
+  /**
+   * The following method is based on logic written by Googlers in the TW
+   * frontend and thus is not included as part of the MIT license.
+   *
+   * Source:
+   * module$exports$google3$customer_support$content$ui$client$tailwind$models$message_model$message_model.MessageModel.prototype.canComment
+   */
+  canComment(currentUser) {
+    if (this.isDeleted()) return false;
+    if (this.isTakenDown()) return false;
+    if (currentUser.isAccountDisabled()) return false;
+    if (this.thread.isLocked() &&
+        !currentUser.isAtLeastCommunityManager(this.thread.getForumId())) {
+      return false;
+    }
+    if (this.thread.isSoftLocked() && !currentUser.isAtLeastSilverRole() &&
+        !this.thread.isAuthoredByUser()) {
+      return false;
+    }
+    return true;
+  }
 }